A practical workshop demo repository showcasing how to deploy a web application to a VPS with Docker, Traefik reverse proxy, automatic SSL certificates, and CI/CD with rollback capabilities.
Ship a web app from local development → public HTTPS domain with CI/CD that can automatically rollback on failure.
deploy-workshop/
├── app/ # Demo application (NOT copied to server)
│ ├── Dockerfile # Container definition
│ ├── package.json # Node.js dependencies
│ ├── src/
│ │ └── server.js # Express.js app with /healthz endpoint
│ └── healthcheck.sh # Health check script
├── traefik/ # Traefik configuration (synced to server)
│ ├── traefik.yml # Static Traefik config
│ └── acme/ # ACME certificates (gitignored, created on server)
├── .github/
│ └── workflows/
│ └── ci-deploy.yml # GitHub Actions CI/CD pipeline
├── scripts/ # Helper scripts (synced to server)
│ ├── record_prev.sh # Image tag tracking
│ └── sync-config.sh # Config sync script
├── docs/ # Documentation
│ ├── ssl-https-setup.md # SSL/HTTPS detailed guide
│ └── runbook.md # Deployment runbook
├── docker-compose.yml # Service orchestration (synced to server)
├── deploy.sh # Deployment script with rollback (synced to server)
├── .env.example # Environment variables template
└── README.md # This file
Note: Only config files (docker-compose.yml, deploy.sh, traefik/, scripts/) are synced to the server. App code stays in the repository and is built into Docker images.
- Docker and Docker Compose installed
- A VPS with Docker installed
- A domain name (or use a subdomain)
- GitHub account with repository
-
Clone the repository
git clone <your-repo-url> cd deploy_demo
-
Set up environment variables
cp .env.example .env # Edit .env with your values -
Build and run locally
cd app docker build -t deploy-demo-app:local . cd .. docker compose up -d
-
Test locally
curl http://localhost:3000/healthz
- Purchase a domain (or use a subdomain)
- Point DNS A record to your VPS IP:
Type: A Name: @ (or your subdomain) Value: <your-vps-ip> TTL: 300
-
SSH into your VPS
ssh user@your-vps-ip
-
Install Docker and Docker Compose
# Ubuntu/Debian curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh sudo apt-get install docker-compose-plugin -
Create deployment directory on server
mkdir -p ~/deploy cd ~/deploy
-
Create .env file manually
nano .env # Add the following (adjust values): # APP_IMAGE=your-dockerhub-username/deploy-demo-app:latest # DOMAIN=example.com # EMAIL=you@example.com # TRAEFIK_IMAGE=traefik:v2.10
-
Initial config files will be synced by CI/CD
- On first push to main branch, GitHub Actions will copy config files
- Or manually copy these files once:
docker-compose.yml,deploy.sh,traefik/traefik.yml,scripts/ - Note: App code is NOT copied to server - only Docker images are pulled from Docker Hub
-
Create deploy user (optional but recommended)
sudo useradd -m -s /bin/bash deploy sudo usermod -aG docker deploy sudo su - deploy
Add these secrets to your GitHub repository (Settings → Secrets and variables → Actions):
DOCKERHUB_USERNAME: Your Docker Hub usernameDOCKERHUB_TOKEN: Docker Hub access token (not password)SRV_HOST: Your VPS IP address or domainSSH_USER: SSH username (e.g.,deploy)SSH_KEY: Private SSH key for server accessDOMAIN: Your domain name (e.g.,example.com)DEPLOY_DIR: Deployment directory on server (e.g.,/home/deploy/deploy_demo)
Generate SSH Key:
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_actions
# Add public key to server: ~/.ssh/authorized_keys
# Use private key content as SSH_KEY secretGenerate Docker Hub Access Token:
- Log in to Docker Hub
- Go to Account Settings → Security → New Access Token
- Create a token with read/write permissions
- Copy the token immediately (it won't be shown again)
- Add token to
DOCKERHUB_TOKENsecret - Add your Docker Hub username to
DOCKERHUB_USERNAMEsecret
-
Copy config files to server (one-time setup)
Option A: Let CI/CD do it (recommended)
- Push to main branch - GitHub Actions will sync config files automatically
Option B: Manual copy
# From your local machine scp docker-compose.yml deploy.sh user@your-vps:~/deploy/ scp -r traefik/ user@your-vps:~/deploy/ scp -r scripts/ user@your-vps:~/deploy/ # On server, make scripts executable ssh user@your-vps "chmod +x ~/deploy/deploy.sh ~/deploy/scripts/*.sh"
-
Start Traefik on server
ssh user@your-vps cd ~/deploy docker compose up -d traefik
-
Build and push image (from your local machine)
docker login -u your-dockerhub-username # Enter your Docker Hub access token when prompted docker build -t your-dockerhub-username/deploy-demo-app:latest ./app docker push your-dockerhub-username/deploy-demo-app:latest -
Deploy manually (first time)
ssh user@your-vps cd ~/deploy ./deploy.sh your-dockerhub-username/deploy-demo-app:latest
-
Verify deployment
curl https://your-domain.com/healthz
- Push to
mainbranch - GitHub Actions will automatically:
- Build Docker image
- Push to Docker Hub
- Deploy to server via SSH
- Run smoke test
- Rollback on failure
To demonstrate rollback functionality:
-
Create a broken version (temporarily):
// In app/src/server.js, modify /healthz endpoint: app.get('/healthz', (req, res) => { res.status(500).json({ status: 'unhealthy' }); // Broken! });
-
Commit and push
git add app/src/server.js git commit -m "Break health check for demo" git push origin main -
Watch CI/CD fail and rollback
- GitHub Actions will deploy
- Health check will fail
- Automatic rollback to previous working version
-
Fix and redeploy
# Restore working version git revert HEAD git push origin main
Problem: If you already have nginx or another reverse proxy using ports 80/443, Traefik cannot bind to these ports.
Check for conflicts:
# On your VPS, check what's using ports 80 and 443
sudo netstat -tlnp | grep -E ':(80|443)'
# or
sudo ss -tlnp | grep -E ':(80|443)'Solutions:
Option 1: Use Traefik as Main Reverse Proxy (Recommended)
- Stop nginx container or remove port bindings from nginx
- Let Traefik handle all traffic on ports 80/443
- Route your existing nginx project through Traefik:
# Add to your existing nginx project's docker-compose.yml labels: - "traefik.enable=true" - "traefik.http.routers.nginx-project.rule=Host(`your-existing-domain.com`)" - "traefik.http.routers.nginx-project.entrypoints=websecure" - "traefik.http.routers.nginx-project.tls.certresolver=le" - "traefik.http.services.nginx-project.loadbalancer.server.port=80"
- Connect nginx container to
traefik-network:networks: - traefik-network
Option 2: Use Different Ports for Traefik (Not Recommended)
- Modify
docker-compose.ymlto use different host ports:ports: - "8080:80" # HTTP on 8080 - "8443:443" # HTTPS on 8443
- Note: This breaks standard HTTP/HTTPS and requires custom port access
Option 3: Multiple IP Addresses
- If your VPS has multiple IPs, bind each service to different IPs:
ports: - "1.2.3.4:80:80" # Traefik on IP 1 - "1.2.3.4:443:443" # nginx uses IP 2 (5.6.7.8:80:80)
Option 4: Run nginx on Different Ports
- Change nginx to use ports 8080/8443 internally
- Route through Traefik (see Option 1)
- Let's Encrypt rate limits: Use staging first:
# In traefik.yml, change to: certificatesResolvers: le: acme: caServer: https://acme-staging-v02.api.letsencrypt.org/directory
# Check logs
docker compose logs web
docker compose logs traefik
# Check container status
docker compose ps
# Restart services
docker compose restart web# Test health endpoint directly
curl http://localhost:3000/healthz
# Check container health
docker inspect deploy-demo-app | grep -A 10 Health# Check DNS propagation
dig your-domain.com
nslookup your-domain.com
# Wait for DNS propagation (can take up to 48 hours, usually < 1 hour)- Check Actions logs in GitHub
- Verify all secrets are set correctly
- Ensure SSH key has correct permissions
- Verify server is accessible from GitHub Actions runner
- Automatic SSL: Zero-touch TLS certificate management with Let's Encrypt
- Docker Integration: Automatic service discovery via Docker labels
- Dynamic Configuration: No need to restart proxy when adding services
- Simple Orchestration: Define all services in one file
- Network Management: Automatic networking between containers
- Environment Variables: Easy configuration management
- Integrated CI/CD: Built into GitHub, no external services needed
- Free for Public Repos: Cost-effective for open source projects
- Flexible Workflows: Easy to customize deployment steps
- Image Tagging: Each deployment tagged with git SHA
- Previous Image Tracking:
prev_image.txtstores last working version - Health Check: Validates deployment before marking success
- Automatic Rollback: On health check failure, restore previous image
Traefik automatically handles SSL certificates using Let's Encrypt:
- Automatic Certificate Issuance: Certificates are issued when services start
- Automatic Renewal: Certificates renew 30 days before expiration (90-day lifetime)
- TLS-ALPN-01 Challenge: Uses port 443 only, no need to expose port 80 for validation
- HTTP to HTTPS Redirect: All HTTP traffic automatically redirected to HTTPS
- Zero Configuration: Just set
DOMAINandEMAILin.env
For detailed SSL/HTTPS documentation, see: SSL/HTTPS Setup Guide
- Never commit secrets: Use
.envand GitHub Secrets - SSH Key Security: Use deploy keys with restricted access
- Firewall: Only open ports 80, 443, and SSH (22)
- Traefik Dashboard: Disable or add authentication in production
- Rate Limiting: Consider adding rate limits to your app
- Architecture Overview - How the deployment system works
- SSL/HTTPS Setup Guide - Detailed explanation of SSL/HTTPS with Traefik
- Deployment Runbook - Quick reference for deployment operations
- Traefik Documentation
- Docker Compose Documentation
- GitHub Actions Documentation
- Let's Encrypt Documentation
Before the workshop, ensure:
- Repository is public and accessible
- All secrets are configured in GitHub
- VPS is set up with Docker
- Domain DNS is configured
- Traefik is running and certificates are issued
- First deployment is successful
- Rollback demo is tested
This is a demo repository for educational purposes.
This is a workshop demo repository. Feel free to fork and adapt for your own workshops!