Set up Caddy 2 as a reverse proxy using Docker with automatic Let's Encrypt SSL certificates. Deploy containerized web applications behind Caddy with zero-downtime SSL management and built-in load balancing.
Prerequisites
- Root or sudo access
- Docker and Docker Compose installed
- Domain names pointing to server
- Ports 80 and 443 open
What this solves
Caddy 2 automatically handles SSL certificate provisioning, renewal, and HTTPS redirects without manual configuration. Running Caddy in Docker containers provides isolated environments, easy scaling, and simplified deployment management for multiple web applications behind a single reverse proxy.
Step-by-step installation
Install Docker and Docker Compose
Install Docker Engine and Docker Compose to manage containerized applications.
sudo apt update
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
sudo systemctl enable --now docker
Create project directory structure
Organize Caddy configuration files and Docker compose setup in a dedicated directory.
mkdir -p ~/caddy-proxy/{config,data,logs}
cd ~/caddy-proxy
Configure Caddy with Caddyfile
Create the main Caddyfile that defines reverse proxy rules and automatic SSL configuration.
{
email admin@example.com
log {
output file /var/log/caddy/access.log
format json
}
}
Main website
example.com {
reverse_proxy web_app:3000
log {
output file /var/log/caddy/example.log
format json
}
encode gzip
header {
# Security headers
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
API subdomain
api.example.com {
reverse_proxy api_service:8080
log {
output file /var/log/caddy/api.log
format json
}
encode gzip
}
Static files subdomain
static.example.com {
reverse_proxy static_server:80
log {
output file /var/log/caddy/static.log
format json
}
encode gzip
header {
Cache-Control "public, max-age=86400"
}
}
Create Docker Compose configuration
Define the complete Docker stack with Caddy proxy and example web applications.
version: '3.8'
services:
caddy:
image: caddy:2.7-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./config/Caddyfile:/etc/caddy/Caddyfile:ro
- ./data:/data
- ./logs:/var/log/caddy
networks:
- caddy_network
environment:
- CADDY_INGRESS_NETWORKS=caddy_network
healthcheck:
test: ["CMD", "caddy", "validate", "--config", "/etc/caddy/Caddyfile"]
interval: 30s
timeout: 10s
retries: 3
# Example Node.js web application
web_app:
image: node:18-alpine
container_name: web_app
restart: unless-stopped
working_dir: /app
volumes:
- ./apps/web:/app
command: ["npm", "start"]
networks:
- caddy_network
environment:
- NODE_ENV=production
- PORT=3000
expose:
- "3000"
depends_on:
- caddy
# Example API service
api_service:
image: nginx:alpine
container_name: api_service
restart: unless-stopped
volumes:
- ./apps/api/nginx.conf:/etc/nginx/nginx.conf:ro
- ./apps/api/html:/usr/share/nginx/html:ro
networks:
- caddy_network
expose:
- "8080"
depends_on:
- caddy
# Example static file server
static_server:
image: nginx:alpine
container_name: static_server
restart: unless-stopped
volumes:
- ./apps/static:/usr/share/nginx/html:ro
networks:
- caddy_network
expose:
- "80"
depends_on:
- caddy
networks:
caddy_network:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.20.0.0/16
volumes:
caddy_data:
driver: local
caddy_config:
driver: local
Create example web applications
Set up sample applications to demonstrate reverse proxy functionality.
mkdir -p apps/{web,api,static}
Create simple Node.js app
cat > apps/web/package.json << 'EOF'
{
"name": "example-web-app",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.0"
}
}
EOF
Configure Node.js application server
Create a simple Express.js server for testing the reverse proxy setup.
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({
message: 'Hello from Web App!',
timestamp: new Date().toISOString(),
headers: req.headers,
container: 'web_app'
});
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy', uptime: process.uptime() });
});
app.listen(PORT, '0.0.0.0', () => {
console.log(Web app running on port ${PORT});
});
Configure API service nginx
Set up nginx configuration for the API service container.
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format json_combined escape=json
'{
"time_local":"$time_local",
"remote_addr":"$remote_addr",
"remote_user":"$remote_user",
"request":"$request",
"status": "$status",
"body_bytes_sent":"$body_bytes_sent",
"request_time":"$request_time",
"http_referrer":"$http_referer",
"http_user_agent":"$http_user_agent"
}';
access_log /var/log/nginx/access.log json_combined;
error_log /var/log/nginx/error.log warn;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types application/json application/javascript text/css text/xml;
server {
listen 8080;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /api/status {
return 200 '{"service":"api","status":"running","timestamp":"$time_iso8601"}';
add_header Content-Type application/json;
}
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}
Create API service content
Add sample API responses and static content for testing.
# API service HTML content
cat > apps/api/html/index.html << 'EOF'
API Service
API Service
This is the API service running behind Caddy proxy.
EOF
Static files content
cat > apps/static/index.html << 'EOF'
Static Files
Static File Server
Serving static content through Caddy proxy with caching headers.
EOF
Install Node.js dependencies in container
Create a temporary container to install npm dependencies for the web application.
cd apps/web
docker run --rm -v "$(pwd)":/app -w /app node:18-alpine npm install
cd ../../
Configure firewall rules
Open HTTP and HTTPS ports for Caddy to receive traffic.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
Start the Docker stack
Launch all containers with Docker Compose in detached mode.
docker-compose up -d
Check container status
docker-compose ps
View logs
docker-compose logs -f caddy
Configure log rotation
Set up logrotate to manage Caddy log files and prevent disk space issues.
~/caddy-proxy/logs/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 0644 root root
postrotate
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
endscript
}
Configure automatic SSL certificates
Update DNS records
Point your domains to the server IP address before SSL certificate issuance.
# Check DNS resolution
dig +short example.com
dig +short api.example.com
dig +short static.example.com
Monitor SSL certificate provisioning
Watch Caddy logs to verify automatic SSL certificate acquisition from Let's Encrypt.
# Monitor certificate provisioning
docker-compose logs -f caddy | grep -i cert
Check certificate status
docker exec caddy caddy list-certificates
Configure certificate renewal monitoring
Create a monitoring script to check certificate expiration and renewal status.
#!/bin/bash
Certificate monitoring script
LOG_FILE="/var/log/caddy-cert-check.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$DATE] Checking SSL certificates..." >> $LOG_FILE
Get certificate information
docker exec caddy caddy list-certificates --format json > /tmp/caddy-certs.json
Check for certificates expiring in 30 days
if command -v jq >/dev/null 2>&1; then
EXPIRING_CERTS=$(jq -r '.[] | select(.not_after | . != null and (. | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) < (now + 2592000)) | .names[0]' /tmp/caddy-certs.json 2>/dev/null)
if [ ! -z "$EXPIRING_CERTS" ]; then
echo "[$DATE] WARNING: Certificates expiring soon: $EXPIRING_CERTS" >> $LOG_FILE
# Add email notification here if needed
else
echo "[$DATE] All certificates are valid" >> $LOG_FILE
fi
else
echo "[$DATE] jq not installed, skipping expiration check" >> $LOG_FILE
fi
rm -f /tmp/caddy-certs.json
Schedule certificate monitoring
Add a cron job to run certificate monitoring daily.
chmod +x ~/caddy-proxy/scripts/check-certs.sh
Add to crontab
(crontab -l 2>/dev/null; echo "0 6 * ~/caddy-proxy/scripts/check-certs.sh") | crontab -
Deploy web applications behind Caddy proxy
Add custom application container
Extend the Docker Compose configuration to include additional web applications.
version: '3.8'
services:
# WordPress application
wordpress:
image: wordpress:6.4-apache
container_name: wordpress_app
restart: unless-stopped
environment:
WORDPRESS_DB_HOST: wordpress_db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: secure_password_here
WORDPRESS_DB_NAME: wordpress
volumes:
- wordpress_data:/var/www/html
networks:
- caddy_network
expose:
- "80"
depends_on:
- wordpress_db
# WordPress database
wordpress_db:
image: mysql:8.0
container_name: wordpress_db
restart: unless-stopped
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: secure_password_here
MYSQL_ROOT_PASSWORD: root_password_here
volumes:
- wordpress_db_data:/var/lib/mysql
networks:
- caddy_network
command: --default-authentication-plugin=mysql_native_password
# Monitoring dashboard
grafana:
image: grafana/grafana:10.2.0
container_name: grafana
restart: unless-stopped
environment:
- GF_SERVER_ROOT_URL=https://monitoring.example.com
- GF_SECURITY_ADMIN_PASSWORD=admin_password_here
volumes:
- grafana_data:/var/lib/grafana
networks:
- caddy_network
expose:
- "3000"
volumes:
wordpress_data:
wordpress_db_data:
grafana_data:
Update Caddyfile for new applications
Add reverse proxy configurations for the new containerized applications.
# Add these sections to the existing Caddyfile
WordPress blog
blog.example.com {
reverse_proxy wordpress:80
log {
output file /var/log/caddy/blog.log
format json
}
encode gzip
# WordPress specific headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
}
}
Monitoring dashboard
monitoring.example.com {
reverse_proxy grafana:3000
log {
output file /var/log/caddy/monitoring.log
format json
}
encode gzip
}
Deploy updated configuration
Apply the new configuration and restart containers with the additional applications.
# Restart with override configuration
docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d
Reload Caddy configuration
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
Check all services
docker-compose ps
Monitor and troubleshoot Caddy containers
Set up container health monitoring
Create monitoring scripts to track container health and performance metrics.
#!/bin/bash
Container monitoring script
LOG_FILE="/var/log/caddy-monitor.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
ALERT_EMAIL="admin@example.com"
echo "[$DATE] Starting container health check..." >> $LOG_FILE
Check if Docker daemon is running
if ! docker info >/dev/null 2>&1; then
echo "[$DATE] ERROR: Docker daemon is not running" >> $LOG_FILE
exit 1
fi
Check critical containers
CRITICAL_CONTAINERS=("caddy" "web_app")
for container in "${CRITICAL_CONTAINERS[@]}"; do
if ! docker ps --format "table {{.Names}}" | grep -q "^$container$"; then
echo "[$DATE] CRITICAL: Container $container is not running" >> $LOG_FILE
# Attempt to restart
echo "[$DATE] Attempting to restart $container..." >> $LOG_FILE
docker-compose restart $container
sleep 10
# Check if restart was successful
if docker ps --format "table {{.Names}}" | grep -q "^$container$"; then
echo "[$DATE] SUCCESS: Container $container restarted" >> $LOG_FILE
else
echo "[$DATE] FAILED: Container $container restart failed" >> $LOG_FILE
fi
else
echo "[$DATE] OK: Container $container is running" >> $LOG_FILE
fi
done
Check container resource usage
docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}" >> $LOG_FILE
Check SSL certificate status
docker exec caddy caddy list-certificates --format json 2>/dev/null | jq -r '.[] | "Certificate: " + (.names | join(", ")) + " expires: " + .not_after' >> $LOG_FILE 2>/dev/null
echo "[$DATE] Container health check completed" >> $LOG_FILE
Configure log aggregation
Set up centralized logging to collect and analyze logs from all containers. This builds on the Loki and Promtail log aggregation setup for comprehensive monitoring.
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: caddy-logs
static_configs:
- targets:
- localhost
labels:
job: caddy
__path__: /var/log/caddy/*.log
pipeline_stages:
- json:
expressions:
level: level
timestamp: ts
message: msg
method: request.method
uri: request.uri
status: resp_headers.status
- timestamp:
source: timestamp
format: RFC3339Nano
- labels:
level:
method:
status:
- job_name: docker-containers
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: [__meta_docker_container_name]
regex: '/(.*)'
target_label: container
- source_labels: [__meta_docker_container_log_stream]
target_label: stream
Create backup and recovery procedures
Implement automated backup for Caddy configuration and SSL certificates.
#!/bin/bash
Caddy backup script
BACKUP_DIR="/backup/caddy"
DATE=$(date '+%Y%m%d_%H%M%S')
BACKUP_NAME="caddy_backup_$DATE.tar.gz"
RETENTION_DAYS=30
Create backup directory
mkdir -p $BACKUP_DIR
echo "Starting Caddy backup at $(date)"
Create backup archive
tar -czf "$BACKUP_DIR/$BACKUP_NAME" \
-C ~/caddy-proxy \
config/ \
data/ \
docker-compose.yml \
docker-compose.override.yml \
apps/ \
scripts/ \
--exclude='*.log'
if [ $? -eq 0 ]; then
echo "Backup created successfully: $BACKUP_NAME"
# Verify backup integrity
tar -tzf "$BACKUP_DIR/$BACKUP_NAME" >/dev/null
if [ $? -eq 0 ]; then
echo "Backup verification passed"
else
echo "ERROR: Backup verification failed"
exit 1
fi
else
echo "ERROR: Backup creation failed"
exit 1
fi
Clean up old backups
find $BACKUP_DIR -name "caddy_backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete
echo "Backup completed at $(date)"
Schedule monitoring and backups
Add cron jobs for automated monitoring and backup procedures.
chmod +x ~/caddy-proxy/scripts/monitor-containers.sh
chmod +x ~/caddy-proxy/scripts/backup-caddy.sh
Schedule monitoring every 5 minutes and backups daily
(crontab -l 2>/dev/null; cat << 'EOF'
Caddy monitoring and maintenance
/5 * ~/caddy-proxy/scripts/monitor-containers.sh
0 2 * ~/caddy-proxy/scripts/backup-caddy.sh
0 6 * ~/caddy-proxy/scripts/check-certs.sh
EOF
) | crontab -
Verify your setup
# Check all containers are running
docker-compose ps
Test HTTP to HTTPS redirect
curl -I http://example.com
Test SSL certificate
curl -I https://example.com
Check Caddy configuration
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
View certificate information
docker exec caddy caddy list-certificates
Test reverse proxy functionality
curl -s https://example.com | jq .
curl -s https://api.example.com/api/status | jq .
curl -I https://static.example.com
Check container health
docker exec caddy sh -c 'caddy version && caddy list-certificates | head -5'
Monitor logs in real-time
docker-compose logs -f --tail=50
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| SSL certificate provisioning fails | DNS not pointing to server or port 80/443 blocked | Verify DNS with dig example.com and check firewall rules |
| 502 Bad Gateway errors | Backend container not reachable or wrong port | Check container connectivity with docker exec caddy nc -zv backend_container port |
| Caddy container keeps restarting | Invalid Caddyfile syntax | Validate config: docker exec caddy caddy validate --config /etc/caddy/Caddyfile |
| High memory usage | Too many active connections or log accumulation | Configure log rotation and tune connection limits in Caddyfile |
| Docker network issues | Containers can't communicate on Docker network | Recreate network: docker network rm caddy_network && docker-compose up -d |
| Permission denied errors in logs | Incorrect file ownership in volumes | Fix ownership: sudo chown -R $USER:$USER ~/caddy-proxy/ |
Next steps
- Implement Caddy 2 rate limiting and DDoS protection for enhanced security
- Configure Loki and Promtail for centralized Docker log aggregation to monitor all container logs
- Set up Prometheus and Grafana monitoring stack for comprehensive infrastructure monitoring
- Configure Caddy load balancing with multiple backend servers for high availability
- Implement Caddy OAuth2 authentication with Keycloak for secure access control
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values
DOMAIN="${1:-example.com}"
EMAIL="${2:-admin@${DOMAIN}}"
INSTALL_DIR="/opt/caddy-proxy"
# Usage message
usage() {
echo "Usage: $0 [domain] [email]"
echo "Example: $0 mysite.com admin@mysite.com"
exit 1
}
# Print colored output
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
exit 1
}
# Cleanup function for rollback
cleanup() {
if [ $? -ne 0 ]; then
error "Installation failed. Cleaning up..."
[ -d "$INSTALL_DIR" ] && rm -rf "$INSTALL_DIR"
systemctl stop docker 2>/dev/null || true
fi
}
trap cleanup ERR
# Check prerequisites
check_prerequisites() {
log "[1/10] Checking prerequisites..."
if [ "$EUID" -eq 0 ]; then
error "This script should not be run as root. Run as a regular user with sudo access."
fi
if ! sudo -n true 2>/dev/null; then
error "This script requires sudo access."
fi
# Validate domain format
if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.[a-zA-Z]{2,}$ ]]; then
error "Invalid domain format: $DOMAIN"
fi
# Validate email format
if [[ ! "$EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
error "Invalid email format: $EMAIL"
fi
}
# Auto-detect distribution
detect_distro() {
log "[2/10] Detecting distribution..."
if [ ! -f /etc/os-release ]; then
error "Cannot detect distribution: /etc/os-release not found"
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
FIREWALL_CMD="firewall-cmd"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
FIREWALL_CMD="firewall-cmd"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
FIREWALL_CMD="firewall-cmd"
;;
*)
error "Unsupported distribution: $ID"
;;
esac
log "Detected: $PRETTY_NAME using $PKG_MGR"
}
# Update system packages
update_system() {
log "[3/10] Updating system packages..."
sudo $PKG_UPDATE
}
# Install Docker and Docker Compose
install_docker() {
log "[4/10] Installing Docker..."
if command -v docker >/dev/null 2>&1; then
warn "Docker already installed, skipping..."
return 0
fi
# Install prerequisites
if [ "$PKG_MGR" = "apt" ]; then
sudo $PKG_INSTALL curl ca-certificates gnupg lsb-release
else
sudo $PKG_INSTALL curl ca-certificates
fi
# Install Docker using official script
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
rm get-docker.sh
# Add user to docker group
sudo usermod -aG docker "$USER"
# Enable and start Docker
sudo systemctl enable --now docker
log "Docker installed successfully"
}
# Configure firewall
configure_firewall() {
log "[5/10] Configuring firewall..."
if [ "$PKG_MGR" = "apt" ]; then
if command -v ufw >/dev/null 2>&1; then
sudo ufw --force enable 2>/dev/null || true
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
fi
else
if command -v firewall-cmd >/dev/null 2>&1; then
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload
fi
fi
}
# Create project directory structure
create_directories() {
log "[6/10] Creating project directories..."
sudo mkdir -p "$INSTALL_DIR"/{config,data,logs,apps/{web,api,static}}
sudo chown -R "$USER:$USER" "$INSTALL_DIR"
chmod 755 "$INSTALL_DIR"
chmod 755 "$INSTALL_DIR"/{config,data,logs,apps}
}
# Create Caddyfile
create_caddyfile() {
log "[7/10] Creating Caddyfile..."
cat > "$INSTALL_DIR/config/Caddyfile" << EOF
{
email $EMAIL
log {
output file /var/log/caddy/access.log
format json
}
}
# Main website
$DOMAIN {
reverse_proxy web_app:3000
log {
output file /var/log/caddy/$DOMAIN.log
format json
}
encode gzip
header {
# Security headers
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
# API subdomain
api.$DOMAIN {
reverse_proxy api_service:8080
log {
output file /var/log/caddy/api.$DOMAIN.log
format json
}
encode gzip
}
# Static files subdomain
static.$DOMAIN {
reverse_proxy static_server:80
log {
output file /var/log/caddy/static.$DOMAIN.log
format json
}
encode gzip
header {
Cache-Control "public, max-age=86400"
}
}
EOF
chmod 644 "$INSTALL_DIR/config/Caddyfile"
}
# Create Docker Compose file
create_docker_compose() {
log "[8/10] Creating Docker Compose configuration..."
cat > "$INSTALL_DIR/docker-compose.yml" << 'EOF'
version: '3.8'
services:
caddy:
image: caddy:2.7-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./config/Caddyfile:/etc/caddy/Caddyfile:ro
- ./data:/data
- ./logs:/var/log/caddy
networks:
- caddy_network
environment:
- CADDY_INGRESS_NETWORKS=caddy_network
healthcheck:
test: ["CMD", "caddy", "validate", "--config", "/etc/caddy/Caddyfile"]
interval: 30s
timeout: 10s
retries: 3
# Example Node.js web application
web_app:
image: node:18-alpine
container_name: web_app
restart: unless-stopped
working_dir: /app
command: ["sh", "-c", "echo 'const http=require(\"http\");http.createServer((req,res)=>{res.writeHead(200,{\"Content-Type\":\"text/html\"});res.end(\"<h1>Hello from Web App!</h1>\");}).listen(3000);' > server.js && node server.js"]
networks:
- caddy_network
environment:
- NODE_ENV=production
- PORT=3000
expose:
- "3000"
# Example API service
api_service:
image: nginx:alpine
container_name: api_service
restart: unless-stopped
command: ["sh", "-c", "echo 'server{listen 8080;location /{return 200 \"{\\\"message\\\":\\\"API Service\\\"}\\\";add_header Content-Type application/json;}}' > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"]
networks:
- caddy_network
expose:
- "8080"
# Example static file server
static_server:
image: nginx:alpine
container_name: static_server
restart: unless-stopped
command: ["sh", "-c", "echo '<h1>Static Files Server</h1>' > /usr/share/nginx/html/index.html && nginx -g 'daemon off;'"]
networks:
- caddy_network
expose:
- "80"
networks:
caddy_network:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.20.0.0/16
EOF
chmod 644 "$INSTALL_DIR/docker-compose.yml"
}
# Start services
start_services() {
log "[9/10] Starting services..."
cd "$INSTALL_DIR"
# Use newgrp to activate docker group membership
sg docker -c "docker compose up -d"
# Wait for services to start
sleep 10
}
# Verify installation
verify_installation() {
log "[10/10] Verifying installation..."
cd "$INSTALL_DIR"
# Check if containers are running
if ! sg docker -c "docker compose ps --services --filter 'status=running'" | grep -q caddy; then
error "Caddy container is not running"
fi
# Check if Caddy is responding
if ! curl -s -o /dev/null -w "%{http_code}" http://localhost | grep -q "200\|301\|302"; then
warn "Caddy may not be responding correctly on HTTP"
fi
log "Installation completed successfully!"
log "Caddy is running and will automatically obtain SSL certificates"
log "Configure your DNS to point $DOMAIN, api.$DOMAIN, and static.$DOMAIN to this server"
log "You may need to log out and back in for Docker group membership to take effect"
}
# Main execution
main() {
if [ $# -gt 2 ]; then
usage
fi
check_prerequisites
detect_distro
update_system
install_docker
configure_firewall
create_directories
create_caddyfile
create_docker_compose
start_services
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh