Deploying with Docker and Cloudflare Tunnel - Part 2

16 Dec 2025

Deploying with Docker and Cloudflare Tunnel - Part 2

"From local Docker Compose to production deployment with Cloudflare Tunnel - secure public access without exposing ports"

In Part 1, I built a document management system with eXist-db and Docker. Now it's time to deploy it to production and make it accessible to the world - securely, without opening firewall ports.

Live demo: existdbapp.lioprojects.xyz


Deployment Goals

As a DevOps engineer, I wanted to demonstrate:

  1. Zero-Trust Architecture: No exposed ports, tunnel-based access
  2. Infrastructure as Code: Reproducible deployment
  3. Service Management: Using systemd for production services
  4. DNS Configuration: Proper domain setup
  5. Reverse Proxy Configuration: Nginx handling multiple concerns

The Deployment Architecture

Internet
    ↓
Cloudflare Global Network
    ↓ (Encrypted Tunnel)
Cloudflared (systemd service)
    ↓
Nginx (:8081) → PHP-FPM → eXist-db
    ↑
Docker Compose (internal network)

Why this architecture?


Step 1: Server Preparation

I used a Ubuntu 22.04 VPS (any cloud provider works):

# Update system
sudo apt update && sudo apt upgrade -y

# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Install Docker Compose
sudo apt install docker-compose-plugin

# Verify installation
docker --version
docker compose version

Step 2: Deploy the Application

# Create project directory
mkdir -p ~/exist-project
cd ~/exist-project

# Clone repository (or create files manually)
git clone <your-repo> .

# Create necessary directories
mkdir -p php-app/uploads nginx
chmod 755 php-app/uploads

# Start services
docker compose up -d

# Check status
docker ps
docker logs nginx -f
docker logs exist -f

Verify local access:

curl -I http://localhost:8081
# Should return: HTTP/1.1 200 OK

At this point, the app works locally but isn't accessible from the internet.


Step 3: Install Cloudflare Tunnel

Cloudflare Tunnel (formerly Argo Tunnel) creates an outbound-only connection from your server to Cloudflare's edge network. No inbound firewall rules needed.

Install cloudflared

# Download latest release
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \
  -o /usr/local/bin/cloudflared

# Make executable
chmod +x /usr/local/bin/cloudflared

# Verify installation
cloudflared version

Authenticate with Cloudflare

cloudflared tunnel login

This opens a browser window. Log in to your Cloudflare account and select your domain. A certificate file is saved to "~/.cloudflared/cert.pem".

Create the Tunnel

cloudflared tunnel create lio-existdb

Output:

Created tunnel lio-existdb with id 30da90b7-0238-4155-9473-67a7291a968c

Important: Save this UUID! You'll need it for configuration.

A credentials file is created at:

~/.cloudflared/30da90b7-0238-4155-9473-67a7291a968c.json

Step 4: Configure the Tunnel

Create the configuration file:

sudo mkdir -p /etc/cloudflared
sudo nano /etc/cloudflared/config.yml

config.yml:

tunnel: lio-existdb
credentials-file: /root/.cloudflared/30da90b7-0238-4155-9473-67a7291a968c.json

ingress:
  - hostname: existdbapp.lioprojects.xyz
    service: http://localhost:8081
  - service: http_status:404

What this does:

Copy credentials file:

sudo cp ~/.cloudflared/*.json /root/.cloudflared/

Step 5: DNS Configuration

In Cloudflare Dashboard:

  1. Go to DNS settings for "lioprojects.xyz"

  2. Add a CNAME record:

    Name: existdbapp
    Target: <TUNNEL-ID>.cfargotunnel.com
    Proxy: ON (orange cloud)
    
  3. The tunnel ID is the UUID from step 3

DNS propagation usually takes 1-5 minutes.


Step 6: Route the Tunnel

Connect your tunnel to the DNS record:

cloudflared tunnel route dns lio-existdb existdbapp.lioprojects.xyz

Verify:

cloudflared tunnel info lio-existdb

Step 7: Run as System Service

Why systemd?

# Install as service
sudo cloudflared service install

# Enable auto-start
sudo systemctl enable cloudflared

# Start the service
sudo systemctl start cloudflared

# Check status
sudo systemctl status cloudflared

View logs:

sudo journalctl -u cloudflared -f

You should see:

Connection established
Registered tunnel connection

Step 8: Fix Nginx for Tunnel

Problem: Nginx rejects requests because the "Host" header doesn't match "server_name".

Original config (broken):

server {
    server_name existdbapp.lioprojects.xyz;  # ❌ Too restrictive
    ...
}

Fixed config:

server {
    server_name _;  # ✅ Accepts any Host header
    ...
}

Why? Cloudflare Tunnel may send requests with different Host headers depending on routing. Using _ makes nginx accept any hostname.

Apply changes:

docker compose restart nginx

Step 9: Test Production Deployment

# From anywhere in the world
curl -I https://existdbapp.lioprojects.xyz

# Should return
HTTP/2 200
server: cloudflare

Test upload:

  1. Visit https://existdbapp.lioprojects.xyz
  2. Upload a test document
  3. Verify it appears in the list
  4. Check eXist-db: http://YOUR-SERVER-IP:8080/exist/apps/eXide

Security Considerations

What's Protected

No exposed ports - Only port 22 (SSH) is open
DDoS protection - Cloudflare absorbs attacks
Automatic HTTPS - Cloudflare manages certificates
Rate limiting - Built into Cloudflare (configurable)

What Still Needs Work

⚠️ Authentication - No login system yet
⚠️ eXist-db admin password - Still default (change it!)
⚠️ File upload limits - Should implement size restrictions
⚠️ Input validation - Basic sanitization only

Production hardening checklist:

# 1. Change eXist-db password
docker exec -it exist /exist/bin/client.sh -u admin -P

# 2. Add firewall rules (only if not using tunnel)
sudo ufw allow 22/tcp
sudo ufw enable

# 3. Set up fail2ban for SSH
sudo apt install fail2ban

Monitoring and Maintenance

Docker Health Checks

Add to "docker-compose.yml":

services:
  nginx:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"]
      interval: 30s
      timeout: 10s
      retries: 3

  exist:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/exist"]
      interval: 30s
      timeout: 10s
      retries: 3

Check Service Status

# Docker containers
docker ps
docker stats

# Cloudflare tunnel
sudo systemctl status cloudflared
sudo journalctl -u cloudflared --since "1 hour ago"

# Disk usage
df -h
du -sh ~/exist-project

Backup Strategy

# Backup eXist-db data
docker exec exist java -jar /exist/start.jar backup \
  -u admin -p "" -b /tmp/backup -c /db

# Copy backup out
docker cp exist:/tmp/backup ./backups/$(date +%Y%m%d)

# Backup Docker volumes
docker run --rm -v exist-project_exist-data:/data \
  -v $(pwd)/backups:/backup alpine \
  tar czf /backup/exist-data-$(date +%Y%m%d).tar.gz /data

Automate with cron:

# Edit crontab
crontab -e

# Add daily backup at 2 AM
0 2 * * * /path/to/backup-script.sh

Performance Metrics

After deployment, I measured:

Metric Value
Initial load 380ms (Cloudflare CDN)
Document upload (1MB) 1.2s
List query (50 docs) 85ms
Container memory ~450MB total
CPU idle <5%

Cloudflare Analytics shows:


Troubleshooting Common Issues

Issue 1: "502 Bad Gateway"

Symptoms: Cloudflare shows 502 error

Causes:

Fix:

docker compose ps  # Check containers
docker logs nginx  # Check nginx logs
curl http://localhost:8081  # Test locally

Issue 2: Tunnel Not Connecting

Symptoms: "systemctl status cloudflared" shows errors

Causes:

Fix:

# Validate config
cloudflared tunnel ingress validate

# Test manually
cloudflared tunnel run lio-existdb

# Check DNS
nslookup existdbapp.lioprojects.xyz

Issue 3: Nginx Rejecting Requests

Symptoms: 404 errors, but containers running

Cause: "server_name" mismatch

Fix: Change to "server_name _;" as shown in Step 8


Cost Breakdown

Service Monthly Cost
VPS (2GB RAM, 1 CPU) $5-10
Domain (already owned) $0
Cloudflare Tunnel FREE
Total ~$7/month

Free tier includes:


What's Next

In Part 3, I'll cover:


Key Takeaways for DevOps Interviews

This deployment demonstrates:

Infrastructure as Code - Reproducible with docker-compose.yml
Zero-Trust Architecture - No exposed attack surface
Service Management - systemd for production services
Reverse Proxy Configuration - Nginx best practices
Container Orchestration - Multi-service Docker Compose
DNS & Networking - Understanding of how traffic flows
Troubleshooting - Systematic debugging approach


Try It Yourself

The complete code is on GitHub: link

Deploy in 5 minutes:

git clone <repo>
cd exist-project
docker compose up -d
# Follow Cloudflare steps above

Building in public and documenting my DevOps journey. Follow for more production-ready projects.