How to Set Up Ghost Blog with Docker using Cloudflare Tunnels: A Complete Guide

How to Set Up Ghost Blog with Docker using Cloudflare Tunnels: A Complete Guide
Photo by Glenn Carstens-Peters / Unsplash

How to Set Up Ghost Blog with Docker and Cloudflare: A Complete Guide

Setting up a Ghost blog with Docker and Cloudflare is one of the best decisions you can make for your self-hosted website. Docker provides easy deployment and portability, while Cloudflare delivers a powerful CDN, DDoS protection, SSL certificates, and performance optimizations that make your Ghost blog faster and more secure. In this guide, I'll walk you through the entire process of deploying Ghost in Docker and connecting it to Cloudflare for optimal performance.

Why Use Docker + Cloudflare with Ghost?

Before we dive into the setup, let's understand the benefits:

Docker Benefits:

  • Easy deployment with docker-compose
  • Portable configuration - move between servers easily
  • Isolated environment - no conflicts with other applications
  • Simple updates - pull new image and restart
  • Consistent setup - works the same everywhere

Cloudflare Benefits:

  • Free SSL certificates with automatic renewal
  • Global CDN that caches your content worldwide
  • DDoS protection and security features
  • Performance optimization with automatic minification
  • Improved load times for international visitors
  • Bandwidth savings on your hosting
  • No need for a reverse proxy - Cloudflare handles everything

Prerequisites

This guide is for Ubuntu 24.04 LTS with Ghost 5.x (Alpine Docker image).

You'll need:

  • Ubuntu 24.04 LTS server
  • Docker and Docker Compose installed
  • A domain name
  • SSH access to your server

Part 1: Setting Up Ghost with Docker

We'll use Ghost's official Docker setup, but skip Caddy since Cloudflare will handle SSL and proxying.

Step 1: Clone the Ghost Docker Repository

# Clone the official Ghost Docker repo
git clone https://github.com/TryGhost/ghost-docker.git /opt/ghost
cd /opt/ghost

Step 2: Configure Environment Variables

Copy the example configuration file:

cp .env.example .env

Edit the .env file:

nano .env

Update these required values:

# Your domain (e.g., blog.redefi.ca)
DOMAIN=blog.redefi.ca

# Generate secure passwords
DATABASE_ROOT_PASSWORD=your_root_password_here  # Generate with: openssl rand -hex 32
DATABASE_PASSWORD=your_db_password_here         # Generate with: openssl rand -hex 32

# SMTP Email Configuration (required for member features)
[email protected]
MAIL_TRANSPORT=SMTP
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_SECURE=false
[email protected]
MAIL_PASS=your_mailgun_password

Step 3: Modify Docker Compose for Cloudflare Tunnel

Since we're using Cloudflare Tunnel, we don't need to expose any ports.

Edit docker-compose.yml:

nano docker-compose.yml

Remove the Caddy service and ensure Ghost ports are NOT exposed (Cloudflare Tunnel will handle connections):

services:
  ghost:
    # ... existing ghost config ...
    # Do NOT expose ports - tunnel handles everything
    # ports:
    #   - "2368:2368"

Step 4: Start Ghost

Run Ghost with Docker Compose:

cd /opt/ghost
docker compose up -d

Check if Ghost is running:

docker compose ps
docker compose logs ghost

Your Ghost blog should now be accessible at http://your-server-ip:2368

Step 5: Initial Ghost Setup

  1. Navigate to http://your-server-ip:2368/ghost/ (or http://localhost:2368/ghost/ if on the server)
  2. Create your admin account
  3. Complete the initial setup

Note: We'll set up the Cloudflare Tunnel next to make it accessible via your domain with HTTPS!

Part 2: Setting Up Cloudflare

Now let's configure Cloudflare to proxy traffic to your Ghost instance.

Step 1: Create a Cloudflare Account

  1. Go to cloudflare.com and sign up for a free account
  2. Verify your email address
  3. Log in to your Cloudflare dashboard

Step 2: Add Your Domain to Cloudflare

  1. Click "Add a Site" in the Cloudflare dashboard
  2. Enter your domain name (e.g., redefi.ca)
  3. Select the Free plan (unless you need advanced features)
  4. Click "Continue"

Step 3: DNS Records Scan

Cloudflare will automatically scan your existing DNS records. This process takes about 60 seconds and imports your current configuration.

Step 4: Update Nameservers

This is the crucial step that points your domain to Cloudflare:

  1. Log in to your domain registrar (GoDaddy, Namecheap, Google Domains, etc.)
  2. Find the nameserver settings (usually under "DNS Management" or "Nameservers")
  3. Replace your current nameservers with Cloudflare's nameservers
  4. Save the changes

Cloudflare will provide you with two nameservers, something like:

ada.ns.cloudflare.com
kai.ns.cloudflare.com

Note: DNS propagation can take up to 24 hours, but usually completes within an hour.

Step 5: Verify Nameserver Change

Back in Cloudflare, click "Done, check nameservers". Cloudflare will verify the change. You'll receive an email once your site is active on Cloudflare.

Part 3: Setting Up Cloudflare Tunnel

Instead of using traditional DNS and exposing ports, we'll use Cloudflare Tunnel for a more secure connection that doesn't require opening any ports on your firewall.

Why Cloudflare Tunnel?

  • No open ports - No need to expose port 2368 to the internet
  • Encrypted connection - Secure tunnel from your server to Cloudflare
  • Dynamic IP friendly - Works even if your IP changes
  • DDoS protection - Built-in Cloudflare security
  • Zero Trust security - Outbound-only connections

Step 1: Create a Cloudflare Tunnel

  1. Log in to your Cloudflare dashboard
  2. Navigate to Zero Trust (or go to https://one.dash.cloudflare.com)
  3. In the left sidebar, click Access → Tunnels
  4. Click Create a tunnel
  5. Give your tunnel a name (e.g., "ghost-blog")
  6. Click Save tunnel

Step 2: Install Cloudflared Connector

Cloudflare will show you installation options. We'll use Docker since we're already using containers.

Copy the Docker command shown (it will look like this):

docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token eyJhIjoiYourTokenHere...

Step 3: Add Cloudflared to Your Docker Compose

Instead of running the command directly, add cloudflared to your Ghost Docker setup:

Edit /opt/ghost/docker-compose.yml:

nano docker-compose.yml

Add the cloudflared service:

services:
  ghost:
    # ... existing ghost config ...
    # Remove or comment out the ports section since we're using tunnel
    # ports:
    #   - "2368:2368"

  db:
    # ... existing db config ...

  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared-tunnel
    restart: unless-stopped
    command: tunnel --no-autoupdate run --token YOUR_TUNNEL_TOKEN_HERE
    depends_on:
      - ghost

Replace YOUR_TUNNEL_TOKEN_HERE with the token from your Docker command.

Step 4: Configure Tunnel Route

Back in the Cloudflare dashboard:

  1. Click Next after installing the connector
  2. Under Public Hostname, add a route:
    • Subdomain: blog
    • Domain: redefi.ca (select your domain)
    • Type: HTTP
    • URL: ghost:2368
  3. Click Save hostname

Important: The URL is ghost:2368 (not localhost) because Docker containers communicate using their service names.

Step 5: Update Firewall (Optional)

Since you're using Cloudflare Tunnel, you can now block port 2368 from external access:

# Remove the previous allow rule
sudo ufw delete allow 2368/tcp

# Or explicitly deny it
sudo ufw deny 2368/tcp

This makes your setup even more secure - Ghost is only accessible through the Cloudflare Tunnel.

Step 6: Start Everything

cd /opt/ghost
docker compose down
docker compose up -d

Check that cloudflared connected successfully:

docker compose logs cloudflared

You should see: "Connection established" or similar success message.

In your Cloudflare dashboard, the tunnel status should show Healthy with a green indicator.

Step 7: Verify Your Setup

  1. Visit your blog at https://blog.redefi.ca
  2. You should see:
    • ✅ Green padlock (HTTPS working automatically)
    • ✅ Your Ghost blog loads
    • ✅ No open ports on your server
    • ✅ Traffic routed through Cloudflare
  3. Test the admin panel at https://blog.redefi.ca/ghost/

Part 4: Cloudflare Optimization Settings

With Cloudflare Tunnel, SSL is automatically handled - no configuration needed! Let's optimize other settings.

Automatic Benefits

Cloudflare Tunnel automatically provides:

  • SSL/TLS encryption - HTTPS works out of the box
  • Valid certificates - No certificate warnings
  • Auto-renewal - Certificates never expire
  • Modern encryption - TLS 1.3 support

Additional Security Settings

Navigate to Security and configure:

Part 5: Verify Ghost Configuration

Your Ghost instance should already be configured with the correct URL from the .env file. Let's verify:

Check Ghost Configuration

  1. Log in to your Ghost admin at https://blog.redefi.ca/ghost/
  2. Go to Settings → General
  3. Verify Site URL shows: https://blog.redefi.ca

If you need to change the URL:

Check logs to ensure it started correctly:

docker compose logs -f ghost

Restart Ghost:

docker compose up -d

Edit your .env file:

nano .env

Update the DOMAIN line:

DOMAIN=blog.redefi.ca

Stop your Ghost containers:

cd /opt/ghost
docker compose down

Test Your Setup

  1. Visit your blog at https://blog.redefi.ca
  2. You should see:
    • ✅ Green padlock (HTTPS working)
    • ✅ Your Ghost blog loads
    • ✅ No mixed content warnings
    • ✅ Images and assets load properly
  3. Test the admin panel at https://blog.redefi.ca/ghost/
    • ✅ Admin loads properly
    • ✅ No redirect loops

Part 6: Optimize Cloudflare for Ghost

Now let's fine-tune Cloudflare settings for optimal Ghost performance.

Caching Configuration

  1. Go to Caching → Configuration
  2. Set Caching Level to "Standard"
  3. Enable Browser Cache TTL: 4 hours (for blogs with frequent updates)
  4. Create a Page Rule for better caching:
    • Go to Rules → Page Rules → Create Page Rule
    • URL pattern: yourdomain.com/*
    • Settings:
      • Cache Level: Cache Everything
      • Edge Cache TTL: 2 hours
      • Browser Cache TTL: 4 hours
    • Save and Deploy
  5. Create a Page Rule to bypass caching for admin:
    • URL pattern: yourdomain.com/ghost/*
    • Settings:
      • Cache Level: Bypass
    • Save and Deploy

Note: Page Rules are applied in order of priority. Make sure the admin bypass rule has higher priority.

Speed Optimization

Navigate to Speed → Optimization and enable:

  • Auto Minify - JavaScript, CSS, HTML
  • Brotli - Better compression than gzip
  • Early Hints - Improves load time
  • Rocket Loader - Async JavaScript loading (test this, may cause issues with some themes)

Security Settings

Go to Security and configure:

  1. Security Level: Medium (or High if you experience bot traffic)
  2. Challenge Passage: 30 minutes
  3. Enable Bot Fight Mode (Free plan)

Under WAF → Custom Rules, create a rule to protect admin:

Field: URI Path
Operator: contains
Value: /ghost/
Action: Managed Challenge (for additional protection)

Firewall Rules

Create firewall rules to protect your Ghost admin:

  1. Go to Security → WAF → Custom Rules
  2. Click Create Rule
  3. Rule name: "Protect Ghost Admin"
  4. Action: Challenge
  5. Deploy

Expression:

(http.request.uri.path contains "/ghost/" and ip.geoip.country ne "CA")

This blocks access to /ghost/ from outside Canada (adjust to your location)

Tip: Use this only if you always access admin from the same country. Otherwise, use a VPN or skip this rule.

Part 7: Testing Your Setup

Verify DNS Propagation

Use these tools to check if your DNS changes have propagated:

  • whatsmydns.net
  • dig yourdomain.com (command line)
  • nslookup yourdomain.com (Windows)

Test SSL Certificate

  1. Visit your site at https://yourdomain.com
  2. Click the padlock icon in your browser
  3. Verify the certificate is issued by Cloudflare
  4. Test SSL configuration at SSL Labs

Check Cloudflare Caching

  1. Visit your homepage
  2. Open browser DevTools (F12)
  3. Go to Network tab
  4. Reload the page
  5. Check response headers for cf-cache-status
    • HIT = Cached (good!)
    • MISS = Not cached yet
    • DYNAMIC = Page rules set to bypass

Verify Page Speed

Test your site speed improvements:

You should see improved load times, especially for international visitors.

Part 8: Advanced Configuration

Custom Cache Rules

For better cache control, create Cache Rules (requires Pro plan or use Page Rules on Free):

Cache CSS and JavaScript:

If: URI Path contains .css or .js
Then: Cache Everything, Edge TTL: 1 week

Cache all image assets longer:

If: URI Path contains .jpg or .png or .webp or .gif
Then: Cache Everything, Edge TTL: 1 month

Email Forwarding

If you want [email protected] to work:

  1. Go to Email → Email Routing
  2. Enable Email Routing
  3. Add destination addresses
  4. Create routing rules (e.g., [email protected][email protected])

Analytics

Cloudflare provides free analytics:

  1. Go to Analytics & Logs
  2. View traffic, security events, performance metrics
  3. Set up Web Analytics for deeper insights (privacy-friendly alternative to Google Analytics)

Troubleshooting Common Issues

Issue: Redirect Loop

Symptoms: Browser shows "too many redirects" error

Solutions:

  1. Verify SSL mode is set to Flexible (not Full or Full Strict)
  2. Check your Ghost URL in docker-compose.yml uses https://
  3. Clear Cloudflare cache: Caching → Configuration → Purge Everything
  4. Clear browser cache and try in incognito mode

Restart Ghost containers:

docker-compose downdocker-compose up -d

Issue: Ghost Container Won't Start

Symptoms: Container keeps restarting or exits immediately

Solutions:

  1. Ensure passwords match between Ghost and MySQL services

Verify Docker network:

docker network lsdocker network inspect ghost-blog_default

Check if port 2368 is already in use:

sudo netstat -tulpn | grep 2368

Verify database connection:

docker-compose logs db

Check container logs:

docker-compose logs ghost

Issue: Admin Panel Not Loading

Symptoms: Can access homepage but /ghost/ doesn't load

Solutions:

  1. Verify Page Rule to bypass cache for /ghost/* exists
  2. Check Firewall rules aren't blocking your IP
  3. Temporarily set "Security Level" to Essentially Off for testing
  4. Clear Cloudflare cache: Caching → Configuration → Purge Everything

Restart Ghost container:

docker-compose restart ghost

Check Ghost logs:

docker-compose logs -f ghost

Issue: Images Not Loading

Symptoms: Text loads but images show as broken

Solutions:

  1. Check for mixed content (HTTP images on HTTPS page)
  2. Enable Automatic HTTPS Rewrites in SSL/TLS settings
  3. Verify Ghost URL in config uses HTTPS
  4. Clear browser and Cloudflare cache

Restart Ghost:

docker-compose restart ghost

Check file permissions in content directory:

ls -la ~/ghost-blog/content/images/

Issue: Can't Access Ghost from Outside

Symptoms: Ghost works locally but not from internet

Solutions:

  1. Verify DNS records are proxied (orange cloud) in Cloudflare

Check if Cloudflare proxy is working:

dig blog.redefi.canslookup blog.redefi.ca

Verify Docker port mapping:

docker-compose ps

Test if Ghost is listening:

curl http://localhost:2368

Check firewall allows port 2368:

sudo ufw status

Issue: Database Connection Errors

Symptoms: Ghost shows database connection errors

Solutions:

  1. Ensure database credentials match in docker-compose.yml

Restart both containers:

docker-compose restart db ghost

Test database connection from Ghost container:

docker-compose exec ghost shnc -zv db 3306

Verify MySQL logs:

docker-compose logs db

Check MySQL container is running:

docker-compose ps db

Issue: Slow Admin Panel

Symptoms: Ghost admin is sluggish

Solutions:

  1. Ensure admin bypass cache rule is active in Cloudflare
  2. Disable Rocket Loader (may interfere with admin JavaScript)
  3. Set Security Level to Low for your IP address
  4. Increase Docker memory limits if needed

Check server resources:

docker stats

Issue: SSL Certificate Not Showing

Symptoms: Browser shows "Not Secure"

Solutions:

  1. Wait 5-10 minutes for Cloudflare to provision certificate
  2. Verify nameservers are correctly set to Cloudflare
  3. Check SSL mode is set to Flexible
  4. Ensure DNS records are proxied (orange cloud enabled)
  5. Try accessing via incognito mode
  6. Check SSL status in Cloudflare dashboard

Issue: Port 2368 Blocked

Symptoms: Cloudflare can't reach your Ghost instance

Solutions:

  1. Ensure your hosting provider doesn't block port 2368

Check Docker logs:

docker-compose logs -f ghost

Temporarily disable firewall for testing:

sudo ufw disable# Test, then re-enablesudo ufw enable

Check firewall rules:

sudo ufw status verbose

Verify Ghost is running:

docker-compose pscurl http://localhost:2368

Maintenance and Best Practices

Regular Tasks

  1. Monitor Cloudflare Analytics - Weekly check for unusual traffic
  2. Review Firewall Events - Monthly review of blocked threats
  3. Update Security Rules - Quarterly review and updates
  4. Clear Cache - After major Ghost updates or theme changes

Check Docker Logs - Weekly review for errors:

docker compose logs --tail=100 ghost

Backup Ghost Content - Weekly backups:

# Backup content directorytar -czf ghost-backup-$(date +%Y%m%d).tar.gz /opt/ghost/ghost-data/# Backup databasedocker compose exec db mysqldump -u root -p ghost > ghost-db-backup-$(date +%Y%m%d).sql

Update Ghost and Docker Images - Monthly updates:

cd /opt/ghostdocker compose pulldocker compose up -d

Docker Management

Clean up old images:

docker image prune -a

Check container status:

docker compose ps

Restart services:

docker compose restart ghost

View logs:

docker compose logs -f ghost

Update Ghost Image:

cd /opt/ghost
docker compose pull
docker compose up -d

Performance Tips

  1. Use WebP images - Convert images to WebP for faster loading
  2. Optimize feature images - Keep under 200KB when possible
  3. Minimize redirects - Each redirect adds latency
  4. Enable HTTP/3 - In Network settings (cutting edge performance)

Monitor Docker resources:

docker stats

Security Recommendations

  1. Enable 2FA - For both Ghost and Cloudflare accounts
  2. Regular audits - Review access logs monthly
  3. Update Ghost - Keep Ghost Docker image current
  4. Backup configuration - Export Cloudflare DNS settings and save .env file
  5. Use strong passwords - 20+ characters with mixed types
  6. Restrict port 2368 - Limit access to Cloudflare IPs only

Backup Strategy

Create automated backups:

#!/bin/bash
# save as ~/ghost-backup.sh

BACKUP_DIR=~/ghost-backups
DATE=$(date +%Y%m%d_%H%M%S)

# Create backup directory
mkdir -p $BACKUP_DIR

# Backup Ghost content
tar -czf $BACKUP_DIR/ghost-content-$DATE.tar.gz /opt/ghost/ghost-data/

# Backup database
docker compose -f /opt/ghost/docker-compose.yml exec -T db mysqldump -u root -pYOUR_ROOT_PASSWORD ghost > $BACKUP_DIR/ghost-db-$DATE.sql

# Keep only last 7 days of backups
find $BACKUP_DIR -type f -mtime +7 -delete

echo "Backup completed: $DATE"

Add to crontab:

crontab -e
# Add this line for daily 2 AM backups
0 2 * * * /bin/bash ~/ghost-backup.sh

Conclusion

Setting up Ghost with Docker and Cloudflare gives you enterprise-level performance and security at minimal cost. Your blog will load faster globally, be more secure against attacks, and handle traffic spikes with ease. The combination of Ghost's excellent CMS, Docker's portability, and Cloudflare's global network creates a powerful, scalable platform for your tech blog.

The initial setup takes about 1-2 hours, including Docker configuration and Cloudflare setup, but the benefits are immediate and long-lasting. Your readers will notice faster load times, and you'll have peace of mind knowing your site is protected by Cloudflare's security features while maintaining full control over your Ghost installation.

Key Benefits of This Setup:

  • ✅ Full control over your Ghost instance
  • ✅ Easy deployment and updates with Docker
  • ✅ Global CDN and DDoS protection via Cloudflare
  • ✅ Free SSL certificates that auto-renew
  • ✅ Portable configuration (move servers easily)
  • ✅ Simple backup and restore process
  • ✅ Minimal hosting costs

Quick Reference Checklist

Docker Setup:

  • ✅ Docker and Docker Compose installed
  • ✅ docker-compose.yml configured with correct domain
  • ✅ Ghost container running on port 2368
  • ✅ MySQL database container running
  • ✅ Firewall configured (port 2368 allowed)
  • ✅ Ghost admin account created

Cloudflare Setup:

  • ✅ Cloudflare account created
  • ✅ Domain added to Cloudflare
  • ✅ Nameservers updated at registrar
  • ✅ A record pointing to server IP (proxied)
  • ✅ SSL mode set to Flexible
  • ✅ Always Use HTTPS enabled
  • ✅ HSTS configured
  • ✅ Page Rules created (cache optimization and admin bypass)
  • ✅ Auto Minify enabled
  • ✅ Security settings configured

Testing:

  • ✅ Site accessible via HTTPS
  • ✅ SSL certificate verified (green padlock)
  • ✅ Ghost admin panel accessible
  • ✅ No redirect loops
  • ✅ Images loading correctly
  • ✅ Performance tested

Additional Resources


This post was published by ReDefi Software Ltd. For more technical guides and insights, subscribe to our newsletter.