Ghost Blog + Docker + Caddy = Chaos? Only If You Miss These Crucial Steps
Ghost is an open-source blogging platform that’s really cool for developers, writers and entrepreneurs. But let me be honest with you — Setting it up on your own VPS with Docker and Caddy can be a frustrating experience.
Recently I was just there, and let’s just say.. it wasn’t all easy task.
In this , blog I’ll show you everything I did (with configs), the DNS issues I faced, the errors I hit, and how I eventually got it working like a charm.
This isn’t just another tutorial — this is the real one, filled with problems, fixes, and “I wish I knew that before” moments.
Why I Didn’t Go With Managed Hosting (And Why You Shouldn’t Either If You Love Control)
Ghost(Pro) is great. But I didn’t want to pay monthly fees when I could just have a VPS and host it myself.
I also wanted:
- Full control over my server
- HTTPS via Caddy instead of Nginx
- Portability using Docker
- The joy (or pain) of debugging things when they go wrong 😅
Step 1: Writing the docker-compose.yml File
Here’s what you can write for docker-compose.yml
:
version: "3.8"
services:
ghost:
image: ghost:5
container_name: ghost
restart: always
ports: - "2368:2368"
volumes: - ./ghost-data:/var/lib/ghost/content
environment:
url: https://yourdomain.com
caddy:
image: caddy
container_name: caddy
restart: always
ports:
- "80:80" - "443:443"
volumes: - ./Caddyfile:/etc/caddy/Caddyfile -
caddy_data:/data - caddy_config:/config
volumes:
caddy_data:
caddy_config:
Looks clean, right? But wait! I quickly ran into trouble when I combined this with DNS and HTTPS…
Step 2: Creating the Caddyfile (Simple, But Powerful)
Here’s my Caddyfile that worked in the end:
yourdomain.com { reverse_proxy ghost:2368 }
This reverse proxies requests from your domain to the Ghost container on port 2368.
Simple? Yes.
Instantly working? Not at all. Here’s where the problem begin.
Step 3: Pointing the Domain (and Why My Site Refused to Load)
I used Cloudflare to manage my domain. Initially, I pointed:
- An A record to my VPS IP
- But I kept getting ERR_TOO_MANY_REDIRECTS and SSL handshake errors
What i found:
- I needed to disable the orange cloud (proxy) in Cloudflare temporarily
- Let Caddy issue the SSL certificate directly
- After SSL was successfully issued, I could re-enable the proxy
But what after that…
Step 4: Ghost Refused to Load Correctly — Here’s the Fix
I noticed the homepage would load but all the internal assets (JS/CSS) broke. That’s because my url in the Ghost environment was wrong at first.
don’t forget to be sure you define the correct url in docker-compose.yml
:
environment: url: https://yourdomain.com
Without this, Ghost thinks it’s running locally and links wrong.
Step 5: Debugging Common Errors (And How I Fixed Them)
Here are some of the exact errors I faced — and how I fixed them:
❌ Error: Caddy fails to issue SSL certificate
- Problem: Cloudflare proxy was ON
- Fix: Temporarily turn it OFF, let Caddy issue the cert, then turn it back ON.
❌ Error: 502 Bad Gateway or Blank Page
- Problem: Caddy couldn’t connect to the Ghost container
- Fix: Make sure Ghost is named correctly in both docker-compose and Caddyfile. For example:
reverse_proxy ghost:2368
Check the container name in docker-compose matches ghost.
❌ Error: Ghost assets not loading (CSS, JS broken)
- Problem: Incorrect url in environment
- Fix: Add the url env var:
environment: url: https://yourdomain.com
❌ Error: DNS is set correctly, but website won’t load
dig yourdomain.com +short
Then check:
curl -I https://yourdomain.com
Bonus Tip: Use docker logs Like a Pro
I ran this constantly while debugging:
docker logs -f ghost docker logs -f caddy
These helped me catch weird redirects, port conflicts, and SSL errors fast.
How Do You Transfer a Docker Volume from One Server to Another and Let Ghost Use It?💡
If you’re migrating your Ghost blog to a new server or backing it up, you’ll need to transfer your Docker volume — the storage that holds your blog posts, images, themes, and settings.
Here are two common methods to do it safely and effectively.
🔁 Method 1: Archive with tar and Transfer
📦 Step 1: Create a Compressed Archive of the Volume
Docker volumes are stored at /var/lib/docker/volumes/
. Archive only the _data folder inside your Ghost volume:
sudo tar -czvf ghost_volume_backup.tar.gz -C /var/lib/docker/volumes/ghost_content/_data .
Replace ghost_content with your actual volume name.
📤 Step 2: Copy the Archive to the New Serve
scp ghost_volume_backup.tar.gz user@new-server-ip:/home/user/
📂 Step 3: Create the Volume and Extract the Archive
On the new server:
docker volume create ghost_content sudo tar -xzvf ghost_volume_backup.tar.gz -C /var/lib/docker/volumes/ghost_content/_data
🔁 Method 2: Use rsync for Direct Volume Sync
This method syncs the volume directly over SSH, skipping the need to create an archive.
🧬 Step 1: Sync the Volume from Old to New Server
sudo rsync -ravz /var/lib/docker/volumes/ghost_content/_data/ user@new-server-ip:/var/lib/docker/volumes/ghost_content/_data/
Ensure the volume is already created on the new server:
docker volume create ghost_content
You may need root access on the destination server to write into Docker’s volume directory.
Here a quick reference table that gives a simple overview of the most common rsync usage:
💡 Quick Explanation of Flags
- -r: recursive (also for subfolders)
- -a: archive (symlinks, keeping permissions)
- -v: verbose (shows progress)
- -z: compress data during transfer (faster over network)
- -n: dry run (simulation)
🧰 Run Ghost and Attach the Volume
You can now run Ghost and mount the volume to /var/lib/ghost/content.
Option A: Using docker run
docker run -d \ --name ghost \ -p 2368:2368 \ -v ghost_content:/var/lib/ghost/content \ ghost:latest
Option B: Using Docker Compose
version: '3'
services:
ghost:
image:
ghost:latest
ports: - "2368:2368"
volumes:
- ghost_content:/var/lib/ghost/content
volumes:
ghost_content:
Start it:
docker-compose up -d
✅ Done!
You’ve successfully:
- Transferred your Ghost content volume
- Preserved blog content, images, and settings
- Launched Ghost with the same data on a new server
Both tar and rsync work great — choose the one that fits your workflow.
💡 Tip: If you’re stuck and can’t log in
If you’re unable to log in because email isn’t working and you want to reset or disable the email verification manually, you can add this to your config:
config.production.json
should be like this:
{ "url": "http://localhost:2368",
"server": { "port": 2368, "host": "::" },
"mail": { "transport": "Direct" },
"logging": { "transports": [ "file", "stdout" ] },
"process": "systemd", "paths": { "contentPath": "/var/lib/ghost/content" },
"security": { "staffDeviceVerification": false } }
Step 1: Copy the Config file from container to your host:
docker cp ghost_container_name:/var/lib/ghost/config.production.json .
Step 2: Edit it on your host:
nano config.production.json
Step 3: Copy it back into the container
docker cp config.production.json ghost_container_name:/var/lib/ghost/config.production.json
Step 4: Restart the container
docker restart ghost_container_name
Then try logging in without 2FA code.
Wrap Up: Was It Worth It?
YES. 100%.
It was frustrating, time-consuming, and required reading through error logs and talking to ChatGPT more than I’d like to admit — but I learned a ton.
Now I have a self-hosted, lightning-fast Ghost blog with SSL, Docker isolation, and a clean Caddy reverse proxy — all for the cost of a cheap VPS.
Final Thoughts
If you’re trying to set up Ghost with Docker and Caddy:
✅ Don’t give up after the first redirect error
✅ Always set the correct url in Ghost’s environment
✅ Let Caddy handle SSL (after DNS is ready and Cloudflare proxy is off)
✅ Use logs and dig to debug
✅ Ask ChatGPT — it might just save you a few hours
You can fuel my next data science deep dive by buying me a coffee ☕!