How to Set Up Ghost Blog with Docker using Cloudflare Tunnels: A Complete Guide
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
- Navigate to
http://your-server-ip:2368/ghost/(orhttp://localhost:2368/ghost/if on the server) - Create your admin account
- 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
- Go to cloudflare.com and sign up for a free account
- Verify your email address
- Log in to your Cloudflare dashboard
Step 2: Add Your Domain to Cloudflare
- Click "Add a Site" in the Cloudflare dashboard
- Enter your domain name (e.g.,
redefi.ca) - Select the Free plan (unless you need advanced features)
- 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:
- Log in to your domain registrar (GoDaddy, Namecheap, Google Domains, etc.)
- Find the nameserver settings (usually under "DNS Management" or "Nameservers")
- Replace your current nameservers with Cloudflare's nameservers
- 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
- Log in to your Cloudflare dashboard
- Navigate to Zero Trust (or go to https://one.dash.cloudflare.com)
- In the left sidebar, click Access → Tunnels
- Click Create a tunnel
- Give your tunnel a name (e.g., "ghost-blog")
- 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:
- Click Next after installing the connector
- Under Public Hostname, add a route:
- Subdomain: blog
- Domain: redefi.ca (select your domain)
- Type: HTTP
- URL:
ghost:2368
- 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
- Visit your blog at
https://blog.redefi.ca - You should see:
- ✅ Green padlock (HTTPS working automatically)
- ✅ Your Ghost blog loads
- ✅ No open ports on your server
- ✅ Traffic routed through Cloudflare
- 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
- Log in to your Ghost admin at
https://blog.redefi.ca/ghost/ - Go to Settings → General
- 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
- Visit your blog at
https://blog.redefi.ca - You should see:
- ✅ Green padlock (HTTPS working)
- ✅ Your Ghost blog loads
- ✅ No mixed content warnings
- ✅ Images and assets load properly
- 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
- Go to Caching → Configuration
- Set Caching Level to "Standard"
- Enable Browser Cache TTL: 4 hours (for blogs with frequent updates)
- 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
- Create a Page Rule to bypass caching for admin:
- URL pattern:
yourdomain.com/ghost/* - Settings:
- Cache Level: Bypass
- Save and Deploy
- URL pattern:
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:
- Security Level: Medium (or High if you experience bot traffic)
- Challenge Passage: 30 minutes
- 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:
- Go to Security → WAF → Custom Rules
- Click Create Rule
- Rule name: "Protect Ghost Admin"
- Action: Challenge
- 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
- Visit your site at
https://yourdomain.com - Click the padlock icon in your browser
- Verify the certificate is issued by Cloudflare
- Test SSL configuration at SSL Labs
Check Cloudflare Caching
- Visit your homepage
- Open browser DevTools (F12)
- Go to Network tab
- Reload the page
- Check response headers for
cf-cache-statusHIT= Cached (good!)MISS= Not cached yetDYNAMIC= 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:
- Go to Email → Email Routing
- Enable Email Routing
- Add destination addresses
- Create routing rules (e.g., [email protected] → [email protected])
Analytics
Cloudflare provides free analytics:
- Go to Analytics & Logs
- View traffic, security events, performance metrics
- 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:
- Verify SSL mode is set to Flexible (not Full or Full Strict)
- Check your Ghost URL in docker-compose.yml uses
https:// - Clear Cloudflare cache: Caching → Configuration → Purge Everything
- 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:
- 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:
- Verify Page Rule to bypass cache for
/ghost/*exists - Check Firewall rules aren't blocking your IP
- Temporarily set "Security Level" to Essentially Off for testing
- 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:
- Check for mixed content (HTTP images on HTTPS page)
- Enable Automatic HTTPS Rewrites in SSL/TLS settings
- Verify Ghost URL in config uses HTTPS
- 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:
- 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:
- 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:
- Ensure admin bypass cache rule is active in Cloudflare
- Disable Rocket Loader (may interfere with admin JavaScript)
- Set Security Level to Low for your IP address
- Increase Docker memory limits if needed
Check server resources:
docker stats
Issue: SSL Certificate Not Showing
Symptoms: Browser shows "Not Secure"
Solutions:
- Wait 5-10 minutes for Cloudflare to provision certificate
- Verify nameservers are correctly set to Cloudflare
- Check SSL mode is set to Flexible
- Ensure DNS records are proxied (orange cloud enabled)
- Try accessing via incognito mode
- Check SSL status in Cloudflare dashboard
Issue: Port 2368 Blocked
Symptoms: Cloudflare can't reach your Ghost instance
Solutions:
- 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
- Monitor Cloudflare Analytics - Weekly check for unusual traffic
- Review Firewall Events - Monthly review of blocked threats
- Update Security Rules - Quarterly review and updates
- 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
- Use WebP images - Convert images to WebP for faster loading
- Optimize feature images - Keep under 200KB when possible
- Minimize redirects - Each redirect adds latency
- Enable HTTP/3 - In Network settings (cutting edge performance)
Monitor Docker resources:
docker stats
Security Recommendations
- Enable 2FA - For both Ghost and Cloudflare accounts
- Regular audits - Review access logs monthly
- Update Ghost - Keep Ghost Docker image current
- Backup configuration - Export Cloudflare DNS settings and save
.envfile - Use strong passwords - 20+ characters with mixed types
- 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.