Docker Compose Reference: Ghost CMS

Type: Software Reference Confidence: 0.93 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

Service Configuration Summary

ServiceImagePortsVolumesKey Env Vars
Ghostghost:5.113.22368 (internal)/var/lib/ghost/contenturl, database__client, database__connection__host, NODE_ENV=production
MySQLmysql:8.03306 (internal)/var/lib/mysqlMYSQL_ROOT_PASSWORD, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE
MariaDBmariadb:10.113306 (internal)/var/lib/mysqlMYSQL_RANDOM_ROOT_PASSWORD=1, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE
Caddycaddy:2-alpine80:80, 443:443/data, /config, CaddyfileAutomatic TLS via Caddyfile
Nginxnginx:1.27-alpine80:80, 443:443/etc/nginx/conf.d, /etc/letsencryptManual cert paths in nginx.conf
Mailgun SMTP(Ghost env)----mail__transport=SMTP, mail__options__host=smtp.mailgun.org, mail__options__port=587
Stripe(Ghost env)----Configure via Ghost Admin > Settings > Memberships
Content Backup(docker exec)--Dump to hostmysqldump -u root -p ghost > backup.sql

Ghost Environment Variable Convention

Config JSON PathEnvironment VariableExample Value
urlurlhttps://blog.example.com
database.clientdatabase__clientmysql
database.connection.hostdatabase__connection__hostdb
database.connection.userdatabase__connection__userghost
database.connection.passworddatabase__connection__passwordsecretpass
database.connection.databasedatabase__connection__databaseghostdb
mail.transportmail__transportSMTP
mail.frommail__from"Blog <[email protected]>"
mail.options.hostmail__options__hostsmtp.mailgun.org
mail.options.portmail__options__port587
mail.options.auth.usermail__options__auth__user[email protected]
mail.options.auth.passmail__options__auth__passmailgun-smtp-password
server.hostserver__host0.0.0.0
server.portserver__port2368
logging.levellogging__levelerror
privacy.useUpdateCheckprivacy__useUpdateCheckfalse

Decision Tree

START: What kind of Ghost Docker deployment do you need?
├── Development/local testing?
│   ├── YES → Use single-container Ghost with SQLite
│   │         docker run -d -p 2368:2368 -e NODE_ENV=development ghost:5.113.2
│   └── NO ↓
├── Production single-server deployment?
│   ├── YES → Use 3-service Compose: Ghost + MySQL/MariaDB + Caddy/Nginx
│   │   ├── Want automatic TLS (easiest)?
│   │   │   ├── YES → Use Caddy reverse proxy
│   │   │   └── NO → Use Nginx + Certbot/Let's Encrypt
│   │   ├── Need transactional email?
│   │   │   ├── YES → Add mail__transport=SMTP env vars
│   │   │   └── NO → Ghost works but admin invites and password resets fail
│   │   └── Need paid memberships?
│   │       ├── YES → Configure Stripe in Ghost Admin after deployment
│   │       └── NO → Skip Stripe setup
│   └── NO ↓
├── Theme development with live reload?
│   ├── YES → Mount local theme directory into Ghost container
│   └── NO ↓
└── DEFAULT → Use the standard 3-service production Compose stack

Step-by-Step Guide

1. Create the project directory and environment file

Set up a directory structure and .env file to store secrets outside the Compose file. [src1]

mkdir -p ~/ghost-blog && cd ~/ghost-blog

# Generate strong passwords
cat > .env << 'EOF'
GHOST_URL=https://blog.example.com
MYSQL_ROOT_PASSWORD=<generate-with-openssl-rand-hex-32>
MYSQL_PASSWORD=<generate-with-openssl-rand-hex-16>
GHOST_DB_USER=ghost
GHOST_DB_NAME=ghostdb
[email protected]
MAIL_PASS=your-mailgun-smtp-password
EOF

Verify: cat .env -- should show all environment variables with generated passwords.

2. Create docker-compose.yml with Ghost, MySQL, and Caddy

Production-ready Compose file with all three services, persistent volumes, and health checks. [src1] [src5]

Full script: docker-compose-production.yml (52 lines)

Verify: docker compose config -- validates syntax and variable substitution.

3. Configure Caddy for automatic TLS

Create a Caddyfile for automatic HTTPS with Let's Encrypt. [src5]

blog.example.com {
    reverse_proxy ghost:2368 {
        lb_try_duration 30s
    }
}

www.blog.example.com {
    redir https://blog.example.com{uri}
}

Verify: After deployment, curl -I https://blog.example.com -- should return HTTP/2 200 with valid TLS.

4. Deploy and initialize Ghost

Start the stack and create your admin account. [src1]

# Start all services
docker compose up -d

# Watch logs until Ghost reports "Ghost booted"
docker compose logs -f ghost

# Once running, visit https://blog.example.com/ghost to create admin account

Verify: docker compose ps -- all services should show status "Up (healthy)".

5. Configure email (Mailgun SMTP)

Add Mailgun SMTP environment variables for transactional emails. [src3] [src5]

# Add to ghost service environment in docker-compose.yml:
environment:
  mail__transport: SMTP
  mail__from: '"My Blog" <[email protected]>'
  mail__options__host: smtp.mailgun.org
  mail__options__port: 587
  mail__options__secure: "false"
  mail__options__auth__user: ${MAIL_USER}
  mail__options__auth__pass: ${MAIL_PASS}

Verify: Ghost Admin > Settings > Email > Send test email -- should arrive in your inbox.

6. Set up automated backups

Create a backup script that dumps both the database and content volume. [src7]

Full script: ghost-backup.sh (28 lines)

Verify: ls ~/ghost-backups/$(date +%Y-%m-%d)/ -- should contain ghost_db.sql.gz and ghost_content.tar.gz.

Code Examples

Docker Compose: Production Stack with Caddy

Full script: docker-compose-production.yml (52 lines)

# Input:  .env file with GHOST_URL, MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD, GHOST_DB_USER, GHOST_DB_NAME
# Output: Running Ghost blog at https://your-domain.com with automatic TLS

services:
  ghost:
    image: ghost:5.113.2
    restart: always
    depends_on:
      db:
        condition: service_healthy
    environment:
      url: ${GHOST_URL}
      database__client: mysql
      database__connection__host: db
      database__connection__user: ${GHOST_DB_USER}
      database__connection__password: ${MYSQL_PASSWORD}
      database__connection__database: ${GHOST_DB_NAME}

Docker Compose: Nginx + Certbot Alternative

Full script: docker-compose-nginx.yml (63 lines)

Bash: Theme Development with Live Reload

# Input:  Local theme directory at ./my-theme
# Output: Ghost running in development mode with theme hot-reload

docker run -d --name ghost-dev \
  -p 2368:2368 \
  -e NODE_ENV=development \
  -e url=http://localhost:2368 \
  -v $(pwd)/my-theme:/var/lib/ghost/content/themes/my-theme \
  ghost:5.113.2

# Restart Ghost to pick up theme changes
docker restart ghost-dev

Bash: Backup and Restore Commands

Full script: ghost-backup.sh (28 lines)

# --- BACKUP ---
docker exec ghost-db mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" ghostdb | gzip > ghost_db.sql.gz

docker run --rm -v ghost_content:/data -v $(pwd):/backup alpine \
  tar czf /backup/ghost_content.tar.gz -C /data .

# --- RESTORE ---
docker compose stop ghost
gunzip < ghost_db.sql.gz | docker exec -i ghost-db mysql -u root -p"$MYSQL_ROOT_PASSWORD" ghostdb
docker run --rm -v ghost_content:/data -v $(pwd):/backup alpine \
  sh -c "rm -rf /data/* && tar xzf /backup/ghost_content.tar.gz -C /data"
docker compose start ghost

Anti-Patterns

Wrong: Using :latest tag in production

# BAD -- :latest can pull breaking changes on next docker compose pull
services:
  ghost:
    image: ghost:latest

Correct: Pin to specific version

# GOOD -- predictable deployments, controlled upgrades
services:
  ghost:
    image: ghost:5.113.2

Wrong: Exposing Ghost port directly to the internet

# BAD -- no TLS, no rate limiting, no security headers
services:
  ghost:
    image: ghost:5.113.2
    ports:
      - "2368:2368"
    environment:
      url: http://blog.example.com:2368

Correct: Use a reverse proxy for TLS termination

# GOOD -- Ghost is only reachable through the reverse proxy
services:
  ghost:
    image: ghost:5.113.2
    # No ports exposed to host
    environment:
      url: https://blog.example.com
  caddy:
    image: caddy:2-alpine
    ports:
      - "80:80"
      - "443:443"

Wrong: Hardcoding secrets in docker-compose.yml

# BAD -- secrets committed to version control
environment:
  database__connection__password: mysecretpassword123
  MYSQL_ROOT_PASSWORD: rootpass456

Correct: Use .env file or Docker secrets

# GOOD -- secrets in .env file (add to .gitignore)
environment:
  database__connection__password: ${MYSQL_PASSWORD}
  MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}

Wrong: No health check on database service

# BAD -- Ghost may start before MySQL is ready
services:
  ghost:
    depends_on:
      - db
  db:
    image: mysql:8.0

Correct: Use depends_on with health check condition

# GOOD -- Ghost waits until MySQL passes health check
services:
  ghost:
    depends_on:
      db:
        condition: service_healthy
  db:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

Common Pitfalls

Diagnostic Commands

# Check all service status
docker compose ps

# View Ghost startup logs
docker compose logs ghost | tail -30

# Check Ghost configuration
docker compose exec ghost ghost config

# Verify volume mounts
docker compose exec ghost ls -la /var/lib/ghost/content/

# Check MySQL health
docker compose exec db mysqladmin ping -u root -p"$MYSQL_ROOT_PASSWORD"

# Monitor resource usage
docker stats ghost-app ghost-db ghost-caddy

# Check Caddy TLS certificate status
docker compose exec caddy caddy list-certificates

# View Nginx error log (if using Nginx)
docker compose exec nginx tail -50 /var/log/nginx/error.log

Version History & Compatibility

Ghost VersionStatusDocker BaseBreaking ChangesNotes
6.x (6.19+)CurrentNode 20 / Alpine 3.23Official Docker Compose tooling, Caddy defaultNew: ActivityPub + Tinybird analytics
5.x (5.113+)LTS / StableNode 18 / Alpine 3.19None from 5.0Most widely deployed; recommended for stability
4.xEOLNode 16Members API changesUpgrade to 5.x first
3.xEOLNode 14Major theme API changesNot supported

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Self-hosting a blog or publication on your own serverYou want zero server managementGhost(Pro) managed hosting
Need full control over data, themes, and integrationsTraffic is under 100 visitors/month and budget is tightStatic site generator (Hugo, Eleventy)
Running multiple Ghost instances (staging + production)You need complex page builder featuresWordPress or Strapi
Want Docker-based reproducible deploymentsYour server has less than 1 GB RAMGhost CLI on bare metal
Need headless CMS with Content API for JamstackYou need e-commerce functionalityShopify or WooCommerce

Important Caveats

Related Units