What This Stack Provides
- Docker: Container runtime for running isolated applications
- Traefik: Reverse proxy with automatic SSL certificates (Let’s Encrypt)
- Portainer: Web UI for managing Docker containers and stacks
- Full domain management: Each service gets its own subdomain with automatic HTTPS
Prerequisites
- Ubuntu 24.04 server (or similar)
- Domain name configured with DNS provider (this guide uses Cloudflare)
- SSH access to your server
- Sudo privileges
Part 1: Install Docker
Important: Don’t use apt install docker.io - it’s outdated!
# Update system
sudo apt update && sudo apt upgrade -y
# Install Docker using official script
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Add your user to docker group (no sudo needed)
sudo usermod -aG docker $USER
# Apply group changes
newgrp docker
# Verify installation
docker --version
docker compose version
You should see Docker version 27.x.x and Docker Compose v2.x.x
Known Issue: Docker 27.4.0
If you encounter API version errors with Traefik, downgrade to 27.3.1:
# Remove current version
sudo apt-get remove docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Install specific version
sudo apt-get update
sudo apt-get install -y \
docker-ce=5:27.3.1-1~ubuntu.24.04~noble \
docker-ce-cli=5:27.3.1-1~ubuntu.24.04~noble \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
# Prevent auto-updates
sudo apt-mark hold docker-ce docker-ce-cli
# Verify version
docker --version
Part 2: Get Cloudflare API Token
- Go to https://dash.cloudflare.com/profile/api-tokens
- Click “Create Token”
- Use template: “Edit zone DNS”
- Set permissions:
- Zone → DNS → Edit
- Zone → Zone → Read
- Zone Resources: Include → Specific zone → yourdomain.com
- Click “Continue to summary” → “Create Token”
- Copy and save the token
Part 3: Install Traefik
Create Docker Network
docker network create traefik
Setup Traefik Directory
mkdir -p ~/traefik/letsencrypt
cd ~/traefik
Create Environment File
nano .env
Add (replace with your values):
DOMAIN=yourdomain.com
CF_API_EMAIL=your-cloudflare-email@example.com
CF_DNS_API_TOKEN=your_cloudflare_api_token_here
Save: Ctrl+X, Y, Enter
Create Traefik Configuration
nano traefik.yml
Add:
api:
dashboard: true
debug: true
entryPoints:
http:
address: ":80"
http:
redirections:
entryPoint:
to: https
scheme: https
https:
address: ":443"
serversTransport:
insecureSkipVerify: true
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
certificatesResolvers:
cloudflare:
acme:
email: your-email@example.com
storage: /letsencrypt/acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"
Important: Change your-email@example.com to your actual email
Create Docker Compose File
nano docker-compose.yml
Add:
services:
traefik:
image: traefik:v3.2
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- traefik
ports:
- "80:80"
- "443:443"
environment:
- DOCKER_API_VERSION=1.44
env_file:
- .env
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
- ./letsencrypt:/letsencrypt
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik-secure.entrypoints=https"
- "traefik.http.routers.traefik-secure.rule=Host(`traefik.${DOMAIN}`)"
- "traefik.http.routers.traefik-secure.tls=true"
- "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.traefik-secure.tls.domains[0].main=${DOMAIN}"
- "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.${DOMAIN}"
- "traefik.http.routers.traefik-secure.service=api@internal"
- "traefik.http.routers.traefik-secure.middlewares=traefik-auth"
- "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$apr1$$xyz123$$abcdef"
networks:
traefik:
external: true
Secure the Dashboard
Generate password hash:
sudo apt install apache2-utils -y
echo $(htpasswd -nb admin yourpassword) | sed -e s/\\$/\\$\\$/g
Copy the output (e.g., admin:$$apr1$$xyz123$$abcdef) and replace the last label in docker-compose.yml:
- "traefik.http.middlewares.traefik-auth.basicauth.users=YOUR_HASH_HERE"
Start Traefik
# Set permissions
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json
# Start Traefik
docker compose up -d
# Check logs
docker logs traefik -f
Add DNS Record
In Cloudflare DNS settings:
- Type: A
- Name: traefik
- Content: your-server-ip
- Proxy status: DNS only (grey cloud)
Wait 1-2 minutes, then visit: https://traefik.yourdomain.com/dashboard/
You should see a login prompt. Enter your username/password.
Part 4: Install Portainer
Setup Portainer Directory
mkdir -p ~/portainer
cd ~/portainer
nano templates.json
# paste this
{
"version": "2",
"templates": []
}
#save and exit
Create Docker Compose File
nano docker-compose.yml
Add (replace yourdomain.com):
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- traefik
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- portainer_data:/data
- ./templates.json:/templates.json:ro --- disable template fetching
command: --templates file:///templates.json --- disable template fetching
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.entrypoints=https"
- "traefik.http.routers.portainer.rule=Host(`portainer.yourdomain.com`)"
- "traefik.http.routers.portainer.tls=true"
- "traefik.http.routers.portainer.tls.certresolver=cloudflare"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
volumes:
portainer_data:
networks:
traefik:
external: true
Start Portainer
docker compose up -d
# Check logs
docker logs portainer -f
Add DNS Record
In Cloudflare DNS settings:
- Type: A
- Name: portainer
- Content: your-server-ip
- Proxy status: DNS only (grey cloud)
Wait 1-2 minutes, then visit: https://portainer.yourdomain.com
First-time setup:
- Create admin username and password
- Choose “Get Started” (local Docker environment)
Part 5: Deploy Your First App
Test Deployment
Create a test app to verify everything works:
mkdir -p ~/test-app
cd ~/test-app
nano docker-compose.yml
Add (replace yourdomain.com):
services:
whoami:
image: traefik/whoami
container_name: test-app
restart: unless-stopped
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.entrypoints=https"
- "traefik.http.routers.whoami.rule=Host(`test.yourdomain.com`)"
- "traefik.http.routers.whoami.tls=true"
- "traefik.http.routers.whoami.tls.certresolver=cloudflare"
networks:
traefik:
external: true
Deploy:
docker compose up -d
Add DNS in Cloudflare:
- Name: test
- Content: your-server-ip
- Proxy: DNS only
Visit: https://test.yourdomain.com - You should see request details!
Deploying Apps from GitHub (Git Push = Auto Deploy)
1. Get GitHub Personal Access Token
- Go to: https://github.com/settings/tokens
- Generate new token → Classic
- Name:
Portainer Deploy - Scopes:
repo(full control) - Generate and copy the token
2. Deploy via Portainer
- Go to
https://portainer.yourdomain.com - Stacks → Add stack
- Name it:
myapp - Choose “Repository”
- Fill in:
- Repository URL:
https://github.com/username/repo - Repository reference:
refs/heads/main - Compose path:
docker-compose.yml - Authentication: Toggle ON
- Username: GitHub username
- Token: Paste token from step 1
- Repository URL:
3. Add docker-compose.yml to Your Repo
In your repo, create docker-compose.yml:
services:
myapp:
build:
context: .
dockerfile: Dockerfile
args:
# Build-time variables (for frontend builds)
- VITE_API_URL=${VITE_API_URL}
restart: unless-stopped
environment:
# Runtime variables (for backend)
- DATABASE_URL=${DATABASE_URL}
- API_KEY=${API_KEY}
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.entrypoints=https"
- "traefik.http.routers.myapp.rule=Host(`myapp.yourdomain.com`)"
- "traefik.http.routers.myapp.tls=true"
- "traefik.http.routers.myapp.tls.certresolver=cloudflare"
- "traefik.http.services.myapp.loadbalancer.server.port=8080"
networks:
- traefik
networks:
traefik:
external: true
4. Set Environment Variables in Portainer
When deploying, add your env vars in Portainer’s “Environment variables” section.
5. Setup Git Push Auto-Deploy
- After deploying, go to stack details in Portainer
- Enable “GitOps updates” → Choose “Webhook”
- Copy the webhook URL
- In GitHub: Settings → Webhooks → Add webhook
- Paste webhook URL, set content type to
application/json - Events: “Just the push event”
- Add webhook
Now every git push triggers auto-deployment!
Common Issues & Solutions
Traefik API Version Error
Error: client version 1.24 is too old
Solution: Downgrade Docker to 27.3.1 (see Part 1)
SSL Certificate Not Generated
Causes:
- DNS not pointing to server (check with
dig traefik.yourdomain.com) - Cloudflare proxy enabled (must be “DNS only” / grey cloud)
- Firewall blocking ports 80/443
Fix:
docker logs traefik -f
Look for ACME certificate generation logs
Service Not Accessible
Checklist:
- DNS record exists and points to server IP
- Container is running:
docker ps - Container is on
traefiknetwork:docker inspect <container> - Traefik labels are correct
- Port is correct in labels
Useful Commands
# View all running containers
docker ps
# View container logs
docker logs <container-name> -f
# Restart a service
cd ~/service-directory
docker compose restart
# Stop all containers
docker compose down
# Remove container and rebuild
docker compose down
docker compose up -d --build
# View Traefik routes
docker exec traefik cat /etc/traefik/traefik.yml
Switching DNS Providers
To switch from Cloudflare to another provider:
- Get API credentials for new provider
- Update
traefik.yml:
certificatesResolvers:
newprovider:
acme:
dnsChallenge:
provider: namecheap # or route53, digitalocean, etc.
- Update environment variables in
.env - Restart Traefik
See full list: https://doc.traefik.io/traefik/https/acme/#providers
Migration to New Server
Backup:
# Backup volumes
docker run --rm -v /var/lib/docker/volumes:/volumes \
-v $(pwd):/backup alpine \
tar -czf /backup/volumes-backup.tar.gz /volumes
# Backup compose files (if not in git)
tar -czf compose-files.tar.gz ~/traefik ~/portainer ~/your-apps
Restore on New Server:
# Install Docker (Part 1)
# Install Traefik & Portainer (Parts 2-4)
# Restore volumes
tar -xzf volumes-backup.tar.gz -C /
# Restore compose files
tar -xzf compose-files.tar.gz -C ~/
# Start everything
cd ~/traefik && docker compose up -d
cd ~/portainer && docker compose up -d
# ... etc
# Update DNS to point to new server IP
Security Best Practices
- Always use secrets for passwords - never hardcode in compose files
- Use strong passwords for Traefik dashboard and Portainer
- Keep Docker updated (but avoid bleeding-edge versions)
- Regular backups of volumes and compose files
- Firewall: Only expose ports 80, 443, and SSH
- SSH key authentication instead of passwords
- Cloudflare proxy can be enabled for additional DDoS protection (after SSL works)
Resources
- Traefik Documentation: https://doc.traefik.io/traefik/
- Portainer Documentation: https://docs.portainer.io/
- Docker Documentation: https://docs.docker.com/
- Cloudflare API: https://developers.cloudflare.com/
Summary
You now have:
- ✅ Automatic SSL certificates for all services (wildcard support)
- ✅ Easy domain management (just add DNS + labels)
- ✅ Web UI for container management (Portainer)
- ✅ Git push = auto deploy workflow
- ✅ Complete control over your infrastructure
- ✅ No service templates forced on you
Next steps: Deploy your applications using the patterns shown above!