How to Deploy Ghost CMS with Docker in 15 Minutes
Introduction
Ghost CMS is one of the best open-source publishing platforms out there — fast, modern, and built for serious content creators. And the good news? With Docker, you can self-host a fully functional Ghost 5 instance in under 15 minutes on your own server.
In this tutorial, I'll walk you through exactly how I deployed the BOTUM blog (blog.botum.ca) using Ghost 5, Docker Compose, Zoraxy as an SSL reverse proxy, and Cloudflare for DNS. No fluff — real commands, real config files, real decisions.
Key Takeaways
- Ghost 5 runs in a lightweight Docker container — no system dependencies to install.
- A single docker-compose.yml file defines your entire environment.
- Your reverse proxy (Zoraxy or Nginx) handles HTTPS — Ghost doesn't need to know about certificates.
- Data persists in /mnt/docker-data/ghost-blog/ and survives container updates.
- Cloudflare DDNS + CNAME blog.botum.ca → botum.ca handles DNS resolution without a static IP.
Prerequisites
Before you start, make sure you have:
- A Linux server (Ubuntu 22.04+ recommended) with SSH access
- Docker and Docker Compose installed (docker compose v2)
- A domain name — in this case blog.botum.ca via Cloudflare
- A reverse proxy with SSL — here Zoraxy (Nginx, Traefik, or Caddy also work)
- A non-root user with sudo access
- Ports 80 and 443 open on your firewall
Step 1 — Create the Directory Structure
Ghost needs persistent storage for its files (themes, images, SQLite DB). Create the directory structure on the host:
mkdir -p /mnt/docker-data/ghost-blog/content
# The content folder will hold: data/, images/, themes/, adapters/Why /mnt/? On this server, /mnt/ is mounted on a separate disk — Ghost data survives a system reinstall.

Step 2 — Create docker-compose.yml
Create the Docker Compose config file. It defines the Ghost container, environment variables, and persistent volumes:
nano /mnt/docker-data/ghost-blog/docker-compose.ymlservices:
ghost-blog:
image: ghost:5
container_name: ghost-blog
restart: unless-stopped
ports:
- "4598:4598"
environment:
url: https://blog.botum.ca
NODE_ENV: production
volumes:
- /mnt/docker-data/ghost-blog/content:/var/lib/ghost/content
- /mnt/docker-data/ghost-blog/config.production.json:/var/lib/ghost/config.production.jsonKey points: restart: unless-stopped automatically restarts the container on reboot. The url variable must exactly match your final public URL with HTTPS — Ghost uses it to generate all internal links.
Step 3 — Create config.production.json
Ghost reads its configuration from this JSON file. Create it before starting the container:
{
"url": "https://blog.botum.ca",
"server": {
"port": 4598,
"host": "0.0.0.0"
},
"database": {
"client": "sqlite3",
"connection": {
"filename": "/var/lib/ghost/content/data/ghost.db"
}
},
"mail": {
"transport": "Direct"
},
"logging": {
"transports": ["stdout"]
},
"process": "local",
"paths": {
"contentPath": "/var/lib/ghost/content"
}
}Step 4 — Launch Ghost with Docker Compose
Start the container in detached mode:
cd /mnt/docker-data/ghost-blog
docker compose up -d
Verify the container is running:
docker ps | grep ghost-blog
# Expected: ghost-blog Up X minutes 0.0.0.0:4598->4598/tcp
# Check logs if needed:
docker logs ghost-blog --tail 50Ghost runs database migrations on first start — wait 30–60 seconds before testing.
Step 5 — Configure Cloudflare DNS
In the Cloudflare dashboard, set up:

- An A record for botum.ca → your server's public IP (enable Cloudflare Proxy ☁️)
- A CNAME record blog → botum.ca (proxied)
- Optional: automatic DDNS via a script or the Cloudflare DDNS addon if your IP changes
Result: blog.botum.ca resolves to your server's IP via Cloudflare, with free DDoS protection and CDN caching.
Step 6 — Configure Zoraxy as SSL Reverse Proxy
Zoraxy is a modern reverse proxy with a web UI and automatic SSL management (Let's Encrypt). In the Zoraxy interface, add a proxy rule:
- Incoming domain: blog.botum.ca
- Destination: http://localhost:4598 (or the server's internal IP)
- SSL: enabled — Zoraxy automatically provisions the Let's Encrypt certificate
- HTTPS redirect: enabled
Ghost runs on port 4598 as plain HTTP — Zoraxy handles TLS termination. Clean separation of concerns: Ghost does content, Zoraxy does security.

Step 7 — First Login and Ghost Setup
Navigate to https://blog.botum.ca/ghost to create your admin account. Complete the setup wizard:
- Blog name and description
- Admin account (email + strong password)
- Theme — the Source theme is installed by default on Ghost 5
Your Ghost blog is now live. Test public access at https://blog.botum.ca — you should see the Ghost homepage.
Useful Maintenance Commands
# Update Ghost to the latest 5.x version
docker compose pull && docker compose up -d
# Backup the SQLite database
cp /mnt/docker-data/ghost-blog/content/data/ghost.db ~/backup-ghost-$(date +%Y%m%d).db
# Follow logs in real time
docker logs ghost-blog -f
# Restart Ghost without losing data
docker compose restart ghost-blogConclusion
In under 15 minutes, you now have a self-hosted Ghost 5 blog, secured with HTTPS, publicly accessible via Cloudflare and Zoraxy. Your data is persistent, the container restarts automatically, and updates are two commands away.
Next steps: configure email sending (Mailgun or SMTP), install a custom theme, enable Ghost members and subscriptions, and set up automatic DB backups.
Take Action
If this tutorial helped you, subscribe to the BOTUM blog to get the next articles on self-hosting, Docker, and open-source tools. Questions about your setup? Drop a comment below — I respond to every message.
Téléchargez ce guide en PDF pour le consulter hors ligne.
⬇ Télécharger le guide (PDF)🚀 Aller plus loin avec BOTUM
Ce guide couvre les bases. En production, chaque environnement a ses spécificités. Les équipes BOTUM accompagnent les organisations dans le déploiement, la configuration avancée et la sécurisation de leur infrastructure. Si vous avez un projet, parlons-en.
Discuter de votre projet →