I deployed Ghost on Dokploy last week. The default docker-compose.yml you find in tutorials works, but it has root DB credentials, plaintext passwords, HTTP URLs, and no mail config. Here's what I changed to make it production-ready.

The Starting Point

The standard compose file looks like this:

version: "3.8"
services:
  ghost:
    image: ghost:6-alpine
    restart: always
    environment:
      database__client: mysql
      database__connection__host: db
      database__connection__user: root
      database__connection__password: example
      database__connection__database: ghost
      url: http://${GHOST_HOST}
    volumes:
      - ghost:/var/lib/ghost/content

  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
    volumes:
      - db:/var/lib/mysql

volumes:
  ghost:
  db:

Three problems jump out immediately:

  1. Ghost connects as MySQL root with password example
  2. url is HTTP — Ghost won't set secure cookies or generate correct links
  3. No mail config means no password resets, no member invites, no newsletter sends

Fix 1: Create a Dedicated DB User

Never run an app as database root. MySQL lets you create a restricted user at container init:

db:
  environment:
    MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    MYSQL_DATABASE: ghost
    MYSQL_USER: ghost_user
    MYSQL_PASSWORD: ${DB_PASSWORD}

Then point Ghost at that user:

ghost:
  environment:
    database__connection__user: ghost_user
    database__connection__password: ${DB_PASSWORD}
    database__connection__database: ghost

Key detail: MYSQL_USER/MYSQL_PASSWORD/MYSQL_DATABASE only run on first container start. If the db volume already exists, MySQL ignores them and keeps the old credentials. On a fresh deploy, delete the volume first (Dokploy UI → Volumes → Delete db) so the new credentials take effect.

Fix 2: Use HTTPS in the URL

Dokploy's Traefik handles SSL termination, but Ghost needs to know it's behind HTTPS:

url: https://${GHOST_HOST}

Without this, you'll get mixed-content warnings, login redirects to HTTP, and broken password-reset links.

Fix 3: Add Mail (Required for Members and Newsletters)

Ghost won't send any email without SMTP configured. Minimum config:

mail__transport: SMTP
mail__options__host: ${SMTP_HOST}
mail__options__port: 587
mail__options__auth__user: ${SMTP_USER}
mail__options__auth__pass: ${SMTP_PASS}
mail__from: ${SMTP_USER}

Providers I've tested:

  • Mailgun — host smtp.mailgun.org, port 587, user is postmaster@mg.yourdomain.com
  • SendGrid — host smtp.sendgrid.net, port 587, user is literally apikey, pass is your SendGrid API key
  • Postmark — host smtp.postmarkapp.com, port 587, user and pass are both your Server API Token

Fix 4: Move Secrets Out of the Compose File

Dokploy has an Environment Variables tab per project. Use it instead of hardcoding.

# In compose — reference only
database__connection__password: ${DB_PASSWORD}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
mail__options__auth__pass: ${SMTP_PASS}
# In Dokploy UI — actual values
GHOST_HOST=yourdomain.com
DB_PASSWORD=9MUu3K801sUyOUxQ6HraDj4O82KKL94P
MYSQL_ROOT_PASSWORD=uwOp5XPvyNN93fL9CIUDzFGvzEUYoj8K
SMTP_HOST=smtp.mailgun.org
SMTP_USER=postmaster@mg.yourdomain.com
SMTP_PASS=your-smtp-password

Click the lock icon next to each secret value in Dokploy so they're masked in logs and the UI.

Fix 5: Isolate the Database on an Internal Network

The DB doesn't need internet access. Only Ghost should talk to it.

services:
  ghost:
    networks:
      - internal
      - web  # Traefik attaches here

  db:
    networks:
      - internal  # no external access

networks:
  internal:
    internal: true
  web:
    external: true

internal: true means no outbound connectivity from that network. The database literally cannot reach the internet.

The Final Compose

version: "3.8"
services:
  ghost:
    image: ghost:6-alpine
    restart: always
    networks:
      - internal
      - web
    environment:
      database__client: mysql
      database__connection__host: db
      database__connection__user: ghost_user
      database__connection__password: ${DB_PASSWORD}
      database__connection__database: ghost
      url: https://${GHOST_HOST}
      mail__transport: SMTP
      mail__options__host: ${SMTP_HOST}
      mail__options__port: 587
      mail__options__auth__user: ${SMTP_USER}
      mail__options__auth__pass: ${SMTP_PASS}
      mail__from: ${SMTP_USER}
    volumes:
      - ghost:/var/lib/ghost/content

  db:
    image: mysql:8.0
    restart: always
    networks:
      - internal
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ghost
      MYSQL_USER: ghost_user
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db:/var/lib/mysql

volumes:
  ghost:
  db:

networks:
  internal:
    internal: true
  web:
    external: true

Generating Strong Passwords

# Run twice — once for DB_PASSWORD, once for MYSQL_ROOT_PASSWORD
openssl rand -base64 32

Example output: 9MUu3K801sUyOUxQ6HraDj4O82KKL94P. Paste both into Dokploy's env vars. Done.

One Gotcha: Image Tags

ghost:6-alpine does not auto-update. It pins to the 6.x series — you get patch and minor updates within v6 when you redeploy, but it never jumps to v7. That's usually what you want. To upgrade, change the tag in compose and redeploy.

Verifying It Works

After deploy, check logs for:

  • Ghost database ready in Xs — DB connected
  • Ghost booted in Xs — app started cleanly
  • No ER_ACCESS_DENIED_ERROR or ECONNREFUSED

Then visit https://yourdomain.com/ghost — first visit shows the admin setup screen. Create your owner account. That's it.

FAQ

Can I use SQLite instead of MySQL?
Only for local dev. SQLite doesn't handle concurrent writes — Ghost Admin and site traffic will lock the database.

My domain shows "Coming soon" after deploy.
Ghost creates a default draft post. Go to /ghost, sign in, and publish or delete it.

How do I back up?
Two parts: database — mysqldump via cron to S3. Content volume — docker run --rm -v ghost_content:/data -v $(pwd):/backup alpine tar czf /backup/ghost-$(date +%F).tar.gz /data. Automate both.

What if I change DB credentials later?
You'll break member login and newsletters because encryption keys are tied to the database. If you must change them, migrate data properly or accept re-onboarding members.


Written by Faisal | Self-hosting Ghost on Dokploy

Last Update: June 28, 2026