This guide outlines how to set up a secure Docker server with Cloudflare for DNS/tunneling and Caddy as a reverse proxy. This setup allows you to host multiple services securely with minimal exposed ports.
First, ensure your system is up to date:
sudo apt update
sudo apt upgrade -yUFW (Uncomplicated Firewall) provides a user-friendly interface to iptables. We'll enable it with only necessary ports open:
# Install UFW if not present
sudo apt install ufw
# Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow necessary ports
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
# Enable firewall
sudo ufw enable
# Verify status
sudo ufw status verboseFail2ban helps protect against brute force attacks by banning IPs that show malicious behavior:
# Install fail2ban
sudo apt install fail2ban
# Create local config
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
# Configure SSH jail
sudo cat << EOF > /etc/fail2ban/jail.d/ssh.local
[sshd]
enabled = true
bantime = 1h
findtime = 1h
maxretry = 3
EOF
# Start and enable fail2ban
sudo systemctl start fail2ban
sudo systemctl enable fail2ban
# Verify status
sudo systemctl status fail2banSecure SSH access with key-based authentication and 2FA:
# Set correct SSH directory permissions
sudo chmod 700 ~/.ssh
sudo chmod 600 ~/.ssh/authorized_keys
# Install Google Authenticator
sudo apt install libpam-google-authenticator
# Configure SSH
sudo nano /etc/ssh/sshd_configAdd/modify these lines in sshd_config:
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
UsePAM yes
AuthenticationMethods publickey,keyboard-interactive
Set up Google Authenticator for your user:
google-authenticatorFollow the prompts and say 'yes' to:
- Time-based tokens
- Update your Google Authenticator file
- Disallow multiple uses
- Increase token window
- Enable rate limiting
Add the following line to /etc/pam.d/sshd:
auth required pam_google_authenticator.so
Restart SSH service:
sudo systemctl restart sshdAdditional system security measures:
# Lock root console login
sudo passwd -l root
# Check for and remove any unnecessary services
sudo systemctl list-unit-files --state=enabledInstall Docker and Docker Compose:
# Install Docker
sudo apt install docker.io
# Add your user to docker group
sudo usermod -aG docker $USER
# Install Docker Compose (if using Ubuntu's docker.io package)
sudo apt install docker-compose
# Note: Recent Docker versions (>27) use 'docker compose' (no hyphen)
# but this setup uses the separate docker-compose commandConfigure GitHub Container Registry authentication:
# Create GitHub Personal Access Token with read:packages scope
# Save it to ~/.github-token
# Login as your user
cat ~/.github-token | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
# Login as root (needed for systemd service)
sudo bash -c 'cat ~/.github-token | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin'This setup uses Cloudflare for DNS management, SSL termination, and secure tunneling to your server.
- Sign up for Cloudflare and add your domain
- Update your domain's nameservers at your registrar to use Cloudflare's nameservers
- Import your existing DNS records if needed
- Set encryption mode to "Full" in SSL/TLS settings
Set up Cloudflare Tunnel to securely expose your services:
- Sign up for Cloudflare Zero Trust
- Create a new tunnel
- Save the tunnel token - you'll need it for the cloudflared container
# Create base directory
sudo mkdir -p /opt/docker
# Set correct ownership so your user can manage files
sudo chown -R $USER:$USER /opt/docker
cd /opt/docker
# Create docker network
docker network create webCreate an environment file for your tunnel token:
# cloudflared/.env
CLOUDFLARE_TUNNEL_TOKEN=<your_tunnel_token>Note: This token can be found in your Cloudflare Zero Trust dashboard after creating a tunnel. The token requires Zone.DNS permissions.
Create directory structure:
mkdir -p cloudflared/{config,data}Create docker-compose.yml:
version: '3.8'
services:
cloudflared:
container_name: cloudflared
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel run
environment:
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
volumes:
- /opt/docker/cloudflared/config:/etc/cloudflared
- /opt/docker/cloudflared/data:/var/log/cloudflared
networks:
- web
networks:
web:
name: web
external: trueIn Cloudflare DNS settings:
- Create a CNAME record for your root domain (example.com) pointing to your tunnel
- Create a wildcard CNAME record (*.example.com) pointing to your tunnel
- In tunnel settings, add two public hostnames matching these records, both proxying to
http://caddy:80
Caddy serves as the reverse proxy for all your services.
Create directory structure:
mkdir -p caddy/{config,data}Create an environment file for your Cloudflare API token:
# caddy/.env
CLOUDFLARE_API_TOKEN=<your_api_token>Note: The Caddy container may require a Cloudflare API token with Zone.Zone and Zone.DNS permissions, though this might have changed in recent versions. You might be able to reuse the same API token as cloudflared - check the current Cloudflare documentation for the most up-to-date requirements.
Create docker-compose.yml:
version: '3.8'
services:
caddy:
container_name: caddy
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
ports:
- 80:80
- 443:443
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
volumes:
- /opt/docker/caddy/data:/data
- /opt/docker/caddy/config:/etc/caddy
networks:
- web
networks:
web:
name: web
external: trueCreate Caddyfile in caddy/config:
{
email your.email@example.com
auto_https off
http_port 80
log {
output file /data/access.log {
roll_size 10mb
roll_keep 10
}
format json
level DEBUG
}
}
(base_config) {
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
Permissions-Policy "interest-cohort=()"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
# Compression
encode gzip
}
:80 {
# Example service configuration
@service1 host service1.example.com
handle @service1 {
import base_config
reverse_proxy service1:3000
}
# Default handler for unknown hosts
handle {
respond "Unknown host" 404
}
}
Create a template for new services:
mkdir -p template/{config,data}Create template/docker-compose.yml:
version: '3.8'
services:
app:
container_name: SERVICE_NAME
image: ghcr.io/username/IMAGE:latest
restart: unless-stopped
networks:
- web
networks:
web:
external: true- Copy the template directory:
cp -r template new-service
cd new-service
rm skip_service # Enable the service-
Update docker-compose.yml with correct service name and image
-
Add service to Caddyfile:
@newservice host newservice.example.com
handle @newservice {
import base_config
reverse_proxy new-service:3000
}
Create start-all.sh:
#!/bin/bash
set -e
ORIGINAL_DIR=$(pwd)
cd "$(dirname "$0")"
for d in */; do
if [ -f "${d}docker-compose.yml" ] && [ ! -f "${d}skip_service" ]; then
echo "Starting ${d%/}..."
(cd "$d" && docker-compose up -d)
fi
done
cd "$ORIGINAL_DIR"Create stop-all.sh:
#!/bin/bash
set -e
ORIGINAL_DIR=$(pwd)
cd "$(dirname "$0")"
for d in */; do
if [ -f "${d}docker-compose.yml" ] && [ ! -f "${d}skip_service" ]; then
echo "Stopping ${d%/}..."
(cd "$d" && docker-compose down)
fi
done
cd "$ORIGINAL_DIR"Make scripts executable:
chmod +x start-all.sh stop-all.shCreate systemd service:
sudo nano /etc/systemd/system/docker-services.serviceAdd content:
[Unit]
Description=Docker Compose Services
Requires=docker.service
After=docker.service network.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/docker
ExecStart=/opt/docker/start-all.sh
ExecStop=/opt/docker/stop-all.sh
[Install]
WantedBy=multi-user.targetEnable and start the service:
sudo systemctl enable docker-services
sudo systemctl start docker-servicesAdd these aliases to your ~/.bashrc or ~/.zshrc for easier management of your Docker services:
# Docker Compose shortcuts
alias up='docker-compose up'
alias down='docker-compose down'
alias restart='docker-compose restart'
# Log monitoring
alias logs="sudo tail /opt/docker/caddy/data/access.log -f"
# Service management
alias start-all="/opt/docker/start-all.sh"
alias stop-all="/opt/docker/stop-all.sh"After adding these aliases, reload your shell configuration:
source ~/.bashrc # or source ~/.zshrcThis completes the setup of your Docker server environment. All services will automatically start on boot and can be managed through the systemd service. New services can be easily added by copying the template and updating the Caddy configuration.