---
# === IDENTITY ===
id: software/devops/docker-ghost/2026
canonical_question: "Docker Compose reference: Ghost CMS"
aliases:
  - "How to deploy Ghost CMS with Docker Compose"
  - "Ghost blog Docker self-hosting setup"
  - "Ghost CMS Docker MySQL Nginx production deployment"
  - "docker-compose ghost mysql caddy reverse proxy"
  - "self-host Ghost blog with Docker"
  - "Ghost Docker environment variables configuration"
  - "Ghost CMS containerized deployment guide"
  - "Ghost Docker Compose with Mailgun SMTP"
entity_type: software_reference
domain: software > devops > Docker Ghost CMS
region: global
jurisdiction: global
temporal_scope: 2025-2026

# === VERIFICATION ===
last_verified: 2026-02-27
confidence: 0.93
version: 1.0
first_published: 2026-02-27

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Ghost 6.0 introduced official Docker Compose tooling with Caddy and optional ActivityPub/Analytics services (2025)"
  next_review: 2026-08-26
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Ghost requires MySQL 8.0+ or MariaDB 10.5+ for production -- SQLite is development only"
  - "The url environment variable MUST match your public domain including https:// -- Ghost uses this for all internal links and redirects"
  - "DATABASE_xxx variables must NOT be changed after initial database creation -- doing so breaks the installation"
  - "Ghost listens on port 2368 internally -- never expose this directly; always use a reverse proxy (Caddy/Nginx) for TLS termination"
  - "Pin Ghost Docker image to a specific version tag (e.g., ghost:5.113.2) -- never use :latest in production"
  - "Ghost content volume (/var/lib/ghost/content) MUST be persisted -- it contains images, themes, and uploaded files"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need WordPress Docker setup instead of Ghost"
    use_instead: "software/devops/docker-wordpress/2026"
  - condition: "Need general Docker Compose tutorial, not Ghost-specific"
    use_instead: "software/devops/docker-compose-basics/2026"
  - condition: "Using Ghost(Pro) managed hosting, not self-hosting"
    use_instead: "Direct Ghost(Pro) documentation at ghost.org/pricing"

# === AGENT HINTS ===
inputs_needed:
  - key: "reverse_proxy"
    question: "Which reverse proxy will you use?"
    type: choice
    options: ["Caddy", "Nginx", "Traefik", "None (development only)"]
  - key: "database"
    question: "Which database engine?"
    type: choice
    options: ["MySQL 8", "MariaDB 10.5+"]
  - key: "mail_provider"
    question: "Which email provider for transactional mail?"
    type: choice
    options: ["Mailgun", "Amazon SES", "Custom SMTP", "None"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/docker-ghost/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-27)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/devops/docker-compose-basics/2026"
      label: "Docker Compose Fundamentals"
    - id: "software/devops/caddy-reverse-proxy/2026"
      label: "Caddy Reverse Proxy Configuration"
  solves: []
  alternative_to:
    - id: "software/devops/docker-wordpress/2026"
      label: "Docker Compose WordPress Setup"
  often_confused_with: []

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "How To Install Ghost With Docker"
    author: Ghost Foundation
    url: https://docs.ghost.org/install/docker
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src2
    title: "Ghost - Official Image | Docker Hub"
    author: Docker Library
    url: https://hub.docker.com/_/ghost
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src3
    title: "Ghost Configuration Reference"
    author: Ghost Foundation
    url: https://docs.ghost.org/config
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src4
    title: "Complete Guide to Self-hosting Ghost CMS With Docker"
    author: Linux Handbook
    url: https://linuxhandbook.com/deploy-ghost-docker/
    type: technical_blog
    published: 2025-03-15
    reliability: high
  - id: src5
    title: "Self Hosting a Ghost blog with docker-compose, Mailgun setup, and Stripe subscriptions"
    author: AJ Friesen
    url: https://www.ajfriesen.com/self-hosting-ghost-with-docker-compose/
    type: technical_blog
    published: 2025-01-20
    reliability: high
  - id: src6
    title: "How to Run Ghost on Docker: Complete Setup Guide"
    author: Draft.dev
    url: https://draft.dev/learn/running-ghost-on-docker-for-your-developer-blog
    type: technical_blog
    published: 2025-10-15
    reliability: high
  - id: src7
    title: "How Can I Backup My Site Data?"
    author: Ghost Foundation
    url: https://docs.ghost.org/faq/manual-backup
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
---

# Docker Compose Reference: Ghost CMS

## TL;DR

- **Bottom line**: Ghost CMS runs in production with a three-service Docker Compose stack -- Ghost (Node.js app on port 2368), MySQL 8 or MariaDB for persistence, and Caddy or Nginx as a TLS-terminating reverse proxy.
- **Key tool/command**: `docker compose up -d` with environment variables for `url`, `database__client`, `database__connection__host`, and `mail__transport`.
- **Watch out for**: The `url` env var must exactly match your public domain with `https://` -- Ghost generates all links from this value, and a mismatch causes redirect loops or broken assets.
- **Works with**: Ghost 5.x/6.x, Docker Engine 20.10+, Docker Compose v2, MySQL 8.0+, MariaDB 10.5+, Node 18/20 (inside the container).

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Ghost requires MySQL 8.0+ or MariaDB 10.5+ for production -- SQLite is development only
- The `url` environment variable MUST match your public domain including `https://` -- Ghost uses this for all internal links and redirects
- `DATABASE_xxx` variables must NOT be changed after initial database creation -- doing so breaks the installation
- Ghost listens on port 2368 internally -- never expose this directly; always use a reverse proxy (Caddy/Nginx) for TLS termination
- Pin Ghost Docker image to a specific version tag (e.g., `ghost:5.113.2`) -- never use `:latest` in production
- Ghost content volume (`/var/lib/ghost/content`) MUST be persisted -- it contains images, themes, and uploaded files

## Quick Reference

| Service | Image | Ports | Volumes | Key Env Vars |
|---|---|---|---|---|
| Ghost | `ghost:5.113.2` | `2368` (internal) | `/var/lib/ghost/content` | `url`, `database__client`, `database__connection__host`, `NODE_ENV=production` |
| MySQL | `mysql:8.0` | `3306` (internal) | `/var/lib/mysql` | `MYSQL_ROOT_PASSWORD`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE` |
| MariaDB | `mariadb:10.11` | `3306` (internal) | `/var/lib/mysql` | `MYSQL_RANDOM_ROOT_PASSWORD=1`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE` |
| Caddy | `caddy:2-alpine` | `80:80`, `443:443` | `/data`, `/config`, `Caddyfile` | Automatic TLS via Caddyfile |
| Nginx | `nginx:1.27-alpine` | `80:80`, `443:443` | `/etc/nginx/conf.d`, `/etc/letsencrypt` | Manual 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 host | `mysqldump -u root -p ghost > backup.sql` |

**Ghost Environment Variable Convention:**

| Config JSON Path | Environment Variable | Example Value |
|---|---|---|
| `url` | `url` | `https://blog.example.com` |
| `database.client` | `database__client` | `mysql` |
| `database.connection.host` | `database__connection__host` | `db` |
| `database.connection.user` | `database__connection__user` | `ghost` |
| `database.connection.password` | `database__connection__password` | `secretpass` |
| `database.connection.database` | `database__connection__database` | `ghostdb` |
| `mail.transport` | `mail__transport` | `SMTP` |
| `mail.from` | `mail__from` | `"Blog <noreply@example.com>"` |
| `mail.options.host` | `mail__options__host` | `smtp.mailgun.org` |
| `mail.options.port` | `mail__options__port` | `587` |
| `mail.options.auth.user` | `mail__options__auth__user` | `postmaster@mg.example.com` |
| `mail.options.auth.pass` | `mail__options__auth__pass` | `mailgun-smtp-password` |
| `server.host` | `server__host` | `0.0.0.0` |
| `server.port` | `server__port` | `2368` |
| `logging.level` | `logging__level` | `error` |
| `privacy.useUpdateCheck` | `privacy__useUpdateCheck` | `false` |

## Decision Tree

```
START: What kind of Ghost Docker deployment do you need?
├── Development/local testing?
│   ├── YES → Use single-container Ghost with SQLite (no DB service needed)
│   │         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 (see Step 1a)
│   │   │   └── NO → Use Nginx + Certbot/Let's Encrypt (see Step 1b)
│   │   ├── Need transactional email?
│   │   │   ├── YES → Add mail__transport=SMTP env vars (see Step 3)
│   │   │   └── NO → Ghost works without mail but admin invites and password resets will fail
│   │   └── Need paid memberships/subscriptions?
│   │       ├── 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 (see Code Examples)
│   └── 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]

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

# Generate strong passwords
cat > .env << 'EOF'
GHOST_URL=https://blog.example.com
MYSQL_ROOT_PASSWORD=$(openssl rand -hex 32)
MYSQL_PASSWORD=$(openssl rand -hex 16)
GHOST_DB_USER=ghost
GHOST_DB_NAME=ghostdb
MAIL_USER=postmaster@mg.example.com
MAIL_PASS=your-mailgun-smtp-password
EOF

# Actually generate the passwords
sed -i "s/\$(openssl rand -hex 32)/$(openssl rand -hex 32)/" .env
sed -i "s/\$(openssl rand -hex 16)/$(openssl rand -hex 16)/" .env
```

**Verify**: `cat .env` -- should show two unique hex strings for passwords.

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

This production-ready Compose file defines all three services with persistent volumes and proper networking. [src1] [src5]

> Full script: [docker-compose-production.yml](scripts/docker-compose-production.yml) (52 lines)

```yaml
# Production Ghost CMS stack
# See full file: scripts/docker-compose-production.yml
services:
  ghost:
    image: ghost:5.113.2
    restart: always
    depends_on:
      db:
        condition: service_healthy
    environment:
      url: ${GHOST_URL}
      database__client: mysql
      # ... (see full script)
```

**Verify**: `docker compose config` -- validates the Compose file syntax and variable substitution.

### 3. Configure Caddy for automatic TLS

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

```text
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. Configure Nginx alternative (if not using Caddy)

If you prefer Nginx with Certbot for SSL, use this configuration instead. [src4] [src6]

> Full script: [nginx-ghost.conf](scripts/nginx-ghost.conf) (31 lines)

```nginx
# Nginx reverse proxy for Ghost
# See full file: scripts/nginx-ghost.conf
server {
    listen 443 ssl http2;
    server_name blog.example.com;
    # ... (see full script)
}
```

**Verify**: `nginx -t` -- should report syntax OK. Then `curl -I https://blog.example.com` -- HTTP/2 200.

### 5. Deploy and initialize Ghost

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

```bash
# 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 three services should show status "Up (healthy)".

### 6. Configure email (Mailgun SMTP)

Add Mailgun SMTP environment variables to enable transactional emails (member signups, password resets, newsletters). [src3] [src5]

```yaml
# Add to ghost service environment in docker-compose.yml:
environment:
  mail__transport: SMTP
  mail__from: '"My Blog" <noreply@mg.example.com>'
  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**: Go to Ghost Admin > Settings > Email > Send test email -- should arrive in your inbox.

### 7. Set up automated backups

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

> Full script: [ghost-backup.sh](scripts/ghost-backup.sh) (28 lines)

```bash
#!/bin/bash
# Ghost backup script -- see full file: scripts/ghost-backup.sh
BACKUP_DIR=~/ghost-backups/$(date +%Y-%m-%d)
mkdir -p "$BACKUP_DIR"
# ... (see full script)
```

**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](scripts/docker-compose-production.yml) (52 lines)

```yaml
# 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
      # ... full config in scripts/
```

### Docker Compose: Nginx + Certbot Alternative

> Full script: [docker-compose-nginx.yml](scripts/docker-compose-nginx.yml) (63 lines)

```yaml
# Input:  .env file + SSL certs from Certbot at /etc/letsencrypt
# Output: Running Ghost blog behind Nginx with Let's Encrypt TLS

services:
  ghost:
    image: ghost:5.113.2
    restart: always
    depends_on:
      db:
        condition: service_healthy
    # ... full config in scripts/
```

### Bash: Theme Development with Live Reload

```bash
# 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

# Watch logs for theme errors
docker logs -f ghost-dev
```

### Bash: Backup and Restore Commands

> Full script: [ghost-backup.sh](scripts/ghost-backup.sh) (28 lines)

```bash
# --- BACKUP ---
# Database dump (compressed)
docker exec ghost-db mysqldump -u root \
  -p"$MYSQL_ROOT_PASSWORD" ghostdb | gzip > ghost_db.sql.gz

# Content volume (images, themes, settings)
docker run --rm -v ghost_content:/data \
  -v $(pwd):/backup alpine \
  tar czf /backup/ghost_content.tar.gz -C /data .
```

```bash
# --- RESTORE ---
# Stop Ghost first
docker compose stop ghost

# Restore database
gunzip < ghost_db.sql.gz | docker exec -i ghost-db \
  mysql -u root -p"$MYSQL_ROOT_PASSWORD" ghostdb

# Restore content volume
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"

# Restart
docker compose start ghost
```

## Anti-Patterns

### Wrong: Using :latest tag in production

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

### Correct: Pin to specific version

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

### Wrong: Exposing Ghost port directly to the internet

```yaml
# 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

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

### Wrong: Hardcoding secrets in docker-compose.yml

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

### Correct: Use .env file or Docker secrets

```yaml
# 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

```yaml
# BAD -- Ghost may start before MySQL is ready, causing connection errors
services:
  ghost:
    depends_on:
      - db
  db:
    image: mysql:8.0
```

### Correct: Use depends_on with health check condition

```yaml
# 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

- **url mismatch causes redirect loops**: Setting `url=http://...` while accessing via HTTPS triggers infinite redirects. Fix: Always set `url=https://your-domain.com` and ensure your reverse proxy passes correct `X-Forwarded-Proto: https` headers. [src3]
- **Database not ready on first boot**: Ghost starts before MySQL finishes initialization, causing "ECONNREFUSED" errors. Fix: Add `healthcheck` to MySQL service and `depends_on: condition: service_healthy` to Ghost. [src6]
- **Content volume not persisted**: Omitting the `/var/lib/ghost/content` volume mount means all images, themes, and uploads are lost on container recreation. Fix: Always mount a named volume or bind mount for this path. [src2]
- **Mailgun region mismatch**: US Mailgun uses `smtp.mailgun.org`, EU uses `smtp.eu.mailgun.org`. Using the wrong host causes silent email failures. Fix: Check your Mailgun dashboard region and set `mail__options__host` accordingly. [src5]
- **MySQL password change after init**: Changing `MYSQL_ROOT_PASSWORD` or `MYSQL_PASSWORD` in `.env` after the database volume is created has no effect -- MySQL only reads these on first initialization. Fix: Either recreate the volume (data loss) or change passwords via SQL commands inside the container. [src1]
- **Ghost upgrade skips major versions**: Jumping from Ghost 4.x to 6.x directly can corrupt the database. Fix: Upgrade one major version at a time (4.x -> 5.x -> 6.x) and backup before each upgrade. [src2]
- **File permission errors after volume restore**: Restored content may have wrong ownership (Ghost runs as UID 1000 / node user). Fix: `docker exec ghost-app chown -R node:node /var/lib/ghost/content`. [src7]
- **Port conflict with existing services**: Port 80 or 443 already in use by Apache or another web server. Fix: Stop conflicting services or change Caddy/Nginx port mappings. [src4]

## Diagnostic Commands

> Full script: [diagnostic-commands.sh](scripts/diagnostic-commands.sh) (26 lines)

```bash
# Check all service status
docker compose ps
# View Ghost startup logs (look for "Ghost booted in X.Xs")
docker compose logs ghost | tail -30
# Test database connectivity from Ghost container
# ... (see full script)
```

## Version History & Compatibility

| Ghost Version | Status | Docker Base | Breaking Changes | Notes |
|---|---|---|---|---|
| 6.x (6.19+) | Current | Node 20 / Alpine 3.23 | Official Docker Compose tooling, Caddy default | New: ActivityPub + Tinybird analytics support |
| 5.x (5.113+) | LTS / Stable | Node 18 / Alpine 3.19 | None from 5.0 | Most widely deployed; recommended for stability |
| 4.x | EOL | Node 16 | Members API changes | Upgrade to 5.x first before going to 6.x |
| 3.x | EOL | Node 14 | Major theme API changes | Not supported |

| Docker Compose | Status | Notes |
|---|---|---|
| Compose V2 (docker compose) | Current | Built into Docker CLI; recommended |
| Compose V1 (docker-compose) | Deprecated | Still works but no longer maintained |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Self-hosting a blog or publication on your own server | You want zero server management | Ghost(Pro) managed hosting |
| Need full control over data, themes, and integrations | Traffic is under 100 visitors/month and budget is tight | Static site generator (Hugo, Eleventy) |
| Running multiple Ghost instances (staging + production) | You need a full CMS with complex page builder features | WordPress or Strapi |
| Want Docker-based reproducible deployments | Your server has less than 1 GB RAM | Use Ghost CLI on bare metal instead |
| Need headless CMS with Content API for Jamstack | You need e-commerce functionality | Shopify or WooCommerce |

## Important Caveats

- Ghost 6.x introduced official Docker Compose tooling with Caddy as the default reverse proxy -- older guides using Nginx may need adaptation for 6.x-specific features (ActivityPub, Tinybird analytics)
- The Alpine-based Ghost image (`ghost:5.113.2-alpine`) is ~150 MB smaller than the Debian variant but may lack some native Node modules -- test your themes and integrations
- Ghost's built-in image optimization (Sharp) requires the full Debian image if you use WebP conversion -- the Alpine variant may need additional packages
- MySQL 8.0 changed the default authentication plugin from `mysql_native_password` to `caching_sha2_password` -- Ghost supports both, but older MySQL client libraries may fail; add `--default-authentication-plugin=mysql_native_password` to MySQL command if needed
- Docker Desktop on macOS/Windows has slower volume I/O than native Linux -- Ghost startup may take 30-60 seconds instead of 5-10 seconds
- Caddy automatically provisions TLS certificates via Let's Encrypt -- ensure port 80 and 443 are open and DNS A record points to your server before starting the stack

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Docker Compose Fundamentals](/software/devops/docker-compose-basics/2026)
- [Caddy Reverse Proxy Configuration](/software/devops/caddy-reverse-proxy/2026)
- [Docker Compose WordPress Setup](/software/devops/docker-wordpress/2026)
