This guide walks through setting up a production-ready VPS with Docker Swarm, Traefik as a load balancer, and automated SSL certificate management. quick-vps-setup
- ✅ Domain Name Configuration
- ✅ Application Deployment
- ✅ TLS + HTTPS + Auto-renewal
- ✅ OpenSSH Hardening
- ✅ Firewall Configuration
- ✅ Load Balancer + High Availability
- ✅ Automated Deployments
- ✅ Monitoring Setup
# Create and configure new user
sudo adduser user
sudo usermod -aG sudo user
# Switch to new user
su - user
# Install tmux for session persistence
sudo apt install tmux
- Enable password authentication temporarily:
sudo nano /etc/ssh/sshd_config
- Copy SSH key from local machine:
ssh-copy-id username@server_ip
- Update SSH configuration for security:
# /etc/ssh/sshd_config
UsePAM no
PasswordAuthentication no
PermitRootLogin no
- Reload SSH service:
sudo systemctl reload ssh
# Update system packages
sudo apt update && sudo apt upgrade -y
# Install required packages
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
# Add Docker's GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Add Docker repository
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io
# Enable Docker service
sudo systemctl enable docker
# Add user to docker group
sudo usermod -aG docker $USER
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enable
sudo ufw status
sudo docker swarm init --advertise-addr $(hostname -I | awk '{print $1}')
docker info | grep Swarm
docker network create --driver=overlay traefik-public
- Create directory and configuration files:
mkdir -p ~/traefik && cd ~/traefik
touch traefik.yml
- Create Traefik configuration (
traefik.yml
):
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
certificatesResolvers:
letsencrypt:
acme:
email: "[email protected]"
storage: "/letsencrypt/acme.json"
httpChallenge:
entryPoint: web
- Create Docker Compose file (
docker-compose.yml
):
version: '3.8'
services:
traefik:
image: traefik:v3.0
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--log.level=INFO"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- traefik-public
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- "node.role==manager"
restart_policy:
condition: any
networks:
traefik-public:
external: true
docker stack deploy -c docker-compose.yml traefik
docker build -t my-next-app .
docker run -p 3000:3000 --env-file .env.local --name my-next-container my-next-app
services:
nextjs:
image: app
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextjs.rule=Host(`your-domain.com`)"
- "traefik.http.routers.nextjs.entrypoints=websecure"
- "traefik.http.routers.nextjs.tls.certresolver=letsencrypt"
networks:
- traefik-public
deploy:
restart_policy:
condition: any
# View service status
docker stack services traefik
# Check logs
docker service logs -f traefik_traefik
# Check SSL/Let's Encrypt logs
docker service logs -f traefik_traefik | grep "letsencrypt"
# Update service
docker stack deploy -c docker-compose.yml app
# Rollback if needed
docker service rollback service_name
# Verify DNS configuration
nslookup your-domain.com
Create or update your GitHub Actions workflow (.github/workflows/deploy.yml
):
name: Build and Push to GitHub Container Registry
on:
push:
branches:
- master
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build the Docker image
run: |
IMAGE_NAME=ghcr.io/${{ github.repository_owner }}/app
COMMIT_HASH=$(git rev-parse --short HEAD)
docker build -t $IMAGE_NAME:latest -t $IMAGE_NAME:$COMMIT_HASH .
- name: Push Docker image to GitHub Container Registry
run: |
IMAGE_NAME=ghcr.io/${{ github.repository_owner }}/app
docker push $IMAGE_NAME:latest
docker push $IMAGE_NAME:$COMMIT_HASH
- name: Deploy to Docker Swarm
run: |
IMAGE_NAME=ghcr.io/${{ github.repository_owner }}/app
ssh -i ${{ secrets.DEPLOY_SSH_KEY }} user@your-server "docker service update --image $IMAGE_NAME:latest app_nextjs"
Update your docker-stack.yml
to use the GitHub Container Registry:
version: '3.8'
services:
nextjs:
image: ghcr.io/your-username/app:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextjs.rule=Host(`your-domain.com`)"
- "traefik.http.routers.nextjs.entrypoints=websecure"
- "traefik.http.routers.nextjs.tls.certresolver=letsencrypt"
- "traefik.http.services.nextjs.loadbalancer.server.port=3000"
networks:
- traefik-public
deploy:
replicas: 2
restart_policy:
condition: any
placement:
constraints:
- "node.role==manager"
networks:
traefik-public:
external: true
# Deploy specific version using commit hash
docker service update --image ghcr.io/your-username/app:<commit_hash> app_nextjs
# Roll back to previous version
docker service rollback app_nextjs