A simplified Docker-based multi-tenant Ghost setup that maximizes resource efficiency by sharing infrastructure services (MySQL, ActivityPub, Tinybird) while keeping each Ghost site isolated.
Based on the official TryGhost/ghost-docker repository by the Ghost Foundation team, extended with multi-tenancy capabilities.
- One shared infrastructure definition: MySQL, Caddy, ActivityPub, Tinybird Local
- One compose file per Ghost site:
docker-compose.yoursite.yml
- Automatic SSL: Caddy handles Let's Encrypt certificates
- Docker-native: No filesystem dependencies, pure container approach
-
Clone and configure:
git clone https://github.com/magicpages/ghost-docker-multi-tenancy.git cd ghost-docker-multi-tenancy cp .env.example .env # Edit .env with your settings
-
Set up DNS records: Point your domains to your server's IP address:
# For each domain you want to use: # Create an A record pointing to your server IP myblog.com IN A YOUR.SERVER.IP.ADDRESS anotherblog.com IN A YOUR.SERVER.IP.ADDRESS # Optional: Add www subdomain (if you want www support) www.myblog.com IN A YOUR.SERVER.IP.ADDRESS www.anotherblog.com IN A YOUR.SERVER.IP.ADDRESS # OR use CNAME: www.myblog.com IN CNAME myblog.com
-
Create your first site:
./scripts/new-site.sh myblog.com
-
Deploy everything:
./scripts/deploy.sh
-
Access your site:
- Site:
https://myblog.com
- Admin:
https://myblog.com/ghost/
- Site:
-
Set up DNS for the new domain:
# Add A record for the new domain anotherblog.com IN A YOUR.SERVER.IP.ADDRESS # Optional: Add www subdomain www.anotherblog.com IN A YOUR.SERVER.IP.ADDRESS
-
Create and deploy the new site:
./scripts/new-site.sh anotherblog.com ./scripts/new-site.sh yetanother.org ./scripts/deploy.sh
Each site gets:
- ✅ Automatic SSL certificate
- ✅ Dedicated MySQL database
- ✅ ActivityPub federation
- ✅ Tinybird Local analytics
- ✅ Isolated content storage
If you prefer to create sites manually:
-
Copy the template:
cp templates/docker-compose.site.template.yml docker-compose.yoursite.yml
-
Edit the file: Replace
DOMAIN
andSITENAME
placeholders -
Add to Caddyfile: Add your domain configuration
-
Deploy:
docker compose -f docker-compose.yml -f docker-compose.yoursite.yml up -d
Configure these in .env
:
# Database (MySQL runs internally - no port exposed to avoid conflicts)
MYSQL_ROOT_PASSWORD=your_secure_root_password
MYSQL_PASSWORD=your_ghost_password
# Tinybird Local Analytics (auto-generated during setup)
TINYBIRD_ADMIN_TOKEN=your_tinybird_admin_token
TINYBIRD_TRACKER_TOKEN=your_tinybird_tracker_token
TINYBIRD_PORT=7181
TRAFFIC_ANALYTICS_PORT=3001
# Email (optional)
MAIL_TRANSPORT=SMTP
MAIL_HOST=smtp.mailgun.org
[email protected]
MAIL_PASSWORD=your_password
# Deploy infrastructure and all sites (creates databases automatically)
./scripts/deploy.sh
# Setup Tinybird Local (called automatically by deploy.sh)
./scripts/setup-tinybird.sh
# Create new site configuration
./scripts/new-site.sh newdomain.com
# View all services
docker compose ps
# View logs for specific site
docker compose logs ghost_myblog_com
# View analytics logs
docker compose logs traffic-analytics
docker compose logs tinybird-local
# Restart specific site
docker compose restart ghost_myblog_com
# Remove site (stop container, keep data)
docker compose stop ghost_myblog_com
All data is stored in Docker volumes:
# Backup MySQL data
docker run --rm -v ghost-docker-multitenancy_mysql_data:/data -v $(pwd):/backup alpine tar czf /backup/mysql-backup.tar.gz /data
# Backup specific site content
docker run --rm -v ghost_myblog_com_content:/data -v $(pwd):/backup alpine tar czf /backup/myblog-content.tar.gz /data
# Backup Caddy certificates
docker run --rm -v ghost-docker-multitenancy_caddy_data:/data -v $(pwd):/backup alpine tar czf /backup/caddy-certs.tar.gz /data
- Resource limits: Each Ghost site limited to 512MB RAM
- Shared MySQL: Single instance handles all sites efficiently
- Caddy caching: Automatic static asset caching
- Health checks: Automatic container recovery
- Multi-tenant services: ActivityPub and Tinybird Local serve all sites
- Automatic HTTPS: Let's Encrypt SSL for all domains
- Security headers: HSTS, anti-clickjacking, XSS protection
- Database isolation: Each site has separate database and user
- Container isolation: Sites can't access each other's data
- Regular updates: Use official Ghost and MySQL images
Site won't start?
docker compose logs ghost_yoursite_com
Database issues?
docker compose exec mysql mysql -u root -p -e "SHOW DATABASES;"
Port conflicts?
- MySQL runs internally only (no exposed port) to avoid conflicts with existing MySQL installations
- If you need external MySQL access, add
ports: ["3306:3306"]
to the mysql service in docker-compose.yml
SSL certificate issues?
docker compose logs caddy
Check site health:
docker compose ps
curl -I https://yoursite.com
Add environment variables to your site compose file:
environment:
# Custom Ghost settings
privacy__useUpdateCheck: false
privacy__useGravatar: false
privacy__useRpcPing: false
# Custom paths
paths__contentPath: /var/lib/ghost/content
Each site automatically gets www redirect options added to the Caddyfile (commented out by default). To enable www redirects:
-
Choose your preferred redirect direction:
# Option 1: www -> non-www redirect (recommended) www.yourdomain.com { redir https://yourdomain.com{uri} permanent } # Option 2: non-www -> www redirect yourdomain.com { redir https://www.yourdomain.com{uri} permanent }
-
Set up DNS for both domains:
yourdomain.com IN A YOUR.SERVER.IP.ADDRESS www.yourdomain.com IN A YOUR.SERVER.IP.ADDRESS # OR use CNAME: www.yourdomain.com IN CNAME yourdomain.com
-
Restart Caddy to apply changes:
docker compose restart caddy
For wildcard subdomain routing, update Caddyfile:
*.yourdomain.com {
@subdomain {
host_regexp ^([^.]+)\.yourdomain\.com$
}
reverse_proxy @subdomain ghost_{re.subdomain.1}:2368
}
Create docker-compose.override.yml
:
version: '3.8'
services:
ghost_yoursite_com:
environment:
NODE_ENV: development
url: http://localhost:2368
ports:
- "2368:2368"
- Export your existing Ghost content
- Create new site with
./scripts/new-site.sh yourdomain.com
- Access Ghost admin and import your content
- Update DNS to point to new server
This setup uses Tinybird Local for privacy-focused analytics:
- ✅ Self-hosted: All analytics data stays on your server
- ✅ Privacy-preserving: Salted user signatures, no external tracking
- ✅ Multi-tenant: Single instance serves all Ghost sites
- ✅ Ghost-native: Uses Ghost's official traffic-analytics service
- ✅ Automatic setup: Schema and tokens generated automatically
The first time you run ./scripts/deploy.sh
, it will:
- Start Tinybird Local on port 7181
- Generate admin and tracker tokens automatically
- Extract Ghost's Tinybird schema from official Ghost image
- Deploy datasources and endpoints to Tinybird Local
- Start Traffic Analytics service on port 3001
- Configure all Ghost sites to use the local instance
- Tinybird API:
http://localhost:7181
(internal) - Traffic Analytics:
http://localhost:3001
(internal) - Ghost Admin: Analytics appear in each Ghost site's admin panel
If you need to run Tinybird setup separately:
./scripts/setup-tinybird.sh
This is useful for:
- Regenerating tokens
- Redeploying schema after Ghost updates
- Troubleshooting analytics issues
This setup is built upon the excellent foundation provided by the official Ghost Docker configuration by the Ghost Foundation
This project extends the MIT-licensed TryGhost/ghost-docker repository. See individual component licenses for full details.