Deploy Docker Apps on Digital Ocean with Caddy for Automatic HTTPS
This guide shows you how to set up a production-ready Docker server on Digital Ocean with automatic HTTPS using Caddy. Unlike traditional setups with Nginx and Certbot, Caddy handles SSL certificates automatically with zero configuration.
Why Caddy Instead of Nginx?
- Automatic HTTPS - Obtains and renews Let's Encrypt certificates automatically
- Zero SSL configuration - No certbot, no cron jobs, no manual renewal
- Simple syntax - Caddyfile is human-readable vs nginx.conf complexity
- Built-in reverse proxy - Perfect for Docker containers
- HTTP/3 support - Modern protocol support out of the box
Architecture Overview
graph TB
subgraph "Internet"
Users[Users/Browsers]
DNS[DNS Provider]
end
subgraph "Digital Ocean Droplet"
subgraph "Docker Network"
Caddy[Caddy Container<br/>Ports 80/443]
App[Your App Container<br/>Port 3000]
end
subgraph "Volumes"
CaddyData[(caddy_data<br/>Certificates)]
AppData[(app_data<br/>Persistent Storage)]
end
end
Users -->|HTTPS| DNS
DNS -->|A Record| Caddy
Caddy -->|Proxy| App
Caddy <-->|Store Certs| CaddyData
App <-->|Persist Data| AppData
style Caddy fill:#22C55E
style App fill:#3B82F6
Step 1: Create a Digital Ocean Droplet
Create the Droplet
- Log into Digital Ocean and click Create > Droplets
- Choose your region (closest to your users)
- Select Ubuntu 24.04 LTS
- Choose a plan:
- Basic $6/mo - Good for small apps
- Basic $12/mo - Recommended for production
- Choose SSH keys for authentication (more secure than password)
- Click Create Droplet
Connect to Your Droplet
ssh root@YOUR_DROPLET_IP
Step 2: Install Docker
Run these commands to install Docker and Docker Compose:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Verify installation
docker --version
docker compose version
Step 3: Configure Your DNS
Before Caddy can obtain SSL certificates, your domain must point to your Droplet.
- Go to your DNS provider (Cloudflare, Namecheap, etc.)
- Add an A Record:
- Name:
@(or your subdomain likeapp) - Value: Your Droplet's IP address
- TTL: Auto or 300
- Name:
Wait a few minutes for DNS propagation.
Step 4: Set Up Your Project
Create Project Directory
mkdir -p /opt/myapp
cd /opt/myapp
Create the Caddyfile
nano Caddyfile
Add the following (replace yourdomain.com with your actual domain):
yourdomain.com {
reverse_proxy app:3000
encode gzip zstd
}
That's it! Caddy will automatically obtain SSL certificates from Let's Encrypt, redirect HTTP to HTTPS, proxy requests to your app container, and enable compression.
Create docker-compose.yml
nano docker-compose.yml
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- web
app:
image: your-app-image:latest
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000
volumes:
- app_data:/data
networks:
- web
networks:
web:
driver: bridge
volumes:
caddy_data:
caddy_config:
app_data:
Step 5: Deploy
docker compose up -d
docker compose logs -f
Within seconds, Caddy will obtain your SSL certificate. Visit https://yourdomain.com and you'll see your app running with HTTPS!
Example: Deploying Flowise AI
Here's a complete example deploying Flowise:
Caddyfile
flowise.yourdomain.com {
reverse_proxy flowise:3000
encode gzip zstd
}
docker-compose.yml
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- web
flowise:
image: flowiseai/flowise:latest
restart: unless-stopped
environment:
- PORT=3000
- FLOWISE_USERNAME=admin
- FLOWISE_PASSWORD=your-secure-password
- DATABASE_PATH=/root/.flowise
- SECRETKEY_PATH=/root/.flowise
- LOG_PATH=/root/.flowise/logs
- BLOB_STORAGE_PATH=/root/.flowise/storage
volumes:
- flowise_data:/root/.flowise
networks:
- web
networks:
web:
volumes:
caddy_data:
caddy_config:
flowise_data:
Multiple Apps on One Server
Caddy can proxy multiple apps on different subdomains:
Caddyfile
flowise.yourdomain.com {
reverse_proxy flowise:3000
encode gzip zstd
}
n8n.yourdomain.com {
reverse_proxy n8n:5678
encode gzip zstd
}
api.yourdomain.com {
reverse_proxy api:8080
encode gzip zstd
}
docker-compose.yml
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- web
flowise:
image: flowiseai/flowise:latest
restart: unless-stopped
volumes:
- flowise_data:/root/.flowise
networks:
- web
n8n:
image: n8nio/n8n:latest
restart: unless-stopped
environment:
- N8N_HOST=n8n.yourdomain.com
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.yourdomain.com
volumes:
- n8n_data:/home/node/.n8n
networks:
- web
api:
image: your-api-image:latest
restart: unless-stopped
volumes:
- api_data:/data
networks:
- web
networks:
web:
volumes:
caddy_data:
caddy_config:
flowise_data:
n8n_data:
api_data:
Advanced Caddyfile Options
Basic Authentication
admin.yourdomain.com {
basicauth * {
admin $2a$14$hashedpassword
}
reverse_proxy admin:3000
}
Generate password hash:
docker run --rm caddy:2-alpine caddy hash-password --plaintext 'your-password'
Custom Headers
yourdomain.com {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
}
reverse_proxy app:3000
}
WebSocket Support
Caddy handles WebSockets automatically, but you can add explicit headers:
yourdomain.com {
reverse_proxy app:3000 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
Useful Commands
# Start containers
docker compose up -d
# Stop containers
docker compose down
# View logs
docker compose logs -f
docker compose logs caddy
# Reload Caddy without restart
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
# Update images
docker compose pull
docker compose up -d
# SSH into container
docker compose exec app sh
Firewall Setup (Recommended)
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status
Troubleshooting
Certificate Issues
docker compose logs caddy
# Common issues:
# - DNS not propagated yet (wait a few minutes)
# - Port 80 blocked by firewall
# - Rate limited by Let's Encrypt (wait 1 hour)
App Not Accessible
docker compose ps
docker compose logs app
docker compose exec caddy ping app
Summary
With Caddy + Docker Compose on Digital Ocean, you get:
- Automatic HTTPS - Zero SSL configuration
- Simple reverse proxy - One line in Caddyfile
- Multiple apps - Easy subdomain routing
- Persistent storage - Docker volumes for data
- Easy updates -
docker compose pull && docker compose up -d - Full control - SSH access to your server
The total setup takes about 10 minutes, and you never have to think about SSL certificates again.
Resources
- Caddy Documentation
- Digital Ocean Droplets Guide
- Docker Compose Documentation
- HTTPS Container Instance on Azure - Similar setup on Azure