Skip to content

Instantly share code, notes, and snippets.

@petemihaylov
Last active April 9, 2025 09:59
Show Gist options
  • Save petemihaylov/cb7704e9ae3cb949eeda7717d6b7e4fc to your computer and use it in GitHub Desktop.
Save petemihaylov/cb7704e9ae3cb949eeda7717d6b7e4fc to your computer and use it in GitHub Desktop.
Setting up a Production Ready VPS

Production VPS Setup Guide

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

Checklist

  • ✅ Domain Name Configuration
  • ✅ Application Deployment
  • ✅ TLS + HTTPS + Auto-renewal
  • ✅ OpenSSH Hardening
  • ✅ Firewall Configuration
  • ✅ Load Balancer + High Availability
  • ✅ Automated Deployments
  • ✅ Monitoring Setup

Initial Server Setup

User Creation and SSH Configuration

# 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

SSH Security Hardening

  1. Enable password authentication temporarily:
sudo nano /etc/ssh/sshd_config
  1. Copy SSH key from local machine:
ssh-copy-id username@server_ip
  1. Update SSH configuration for security:
# /etc/ssh/sshd_config
UsePAM no
PasswordAuthentication no
PermitRootLogin no
  1. Reload SSH service:
sudo systemctl reload ssh

Docker Installation and Configuration

Install Docker and Docker Compose

# 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

Firewall Configuration (UFW)

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enable
sudo ufw status

Docker Swarm and Traefik Setup

Initialize Docker Swarm

sudo docker swarm init --advertise-addr $(hostname -I | awk '{print $1}')
docker info | grep Swarm

Create Traefik Network

docker network create --driver=overlay traefik-public

Configure Traefik

  1. Create directory and configuration files:
mkdir -p ~/traefik && cd ~/traefik
touch traefik.yml
  1. 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
  1. 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

Deploy Traefik Stack

docker stack deploy -c docker-compose.yml traefik

Application Deployment

Build and Run Application

docker build -t my-next-app .
docker run -p 3000:3000 --env-file .env.local --name my-next-container my-next-app

Example Service Configuration with Traefik

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

Maintenance and Troubleshooting

Check Service Status

# 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"

Service Updates and Rollbacks

# Update service
docker stack deploy -c docker-compose.yml app

# Rollback if needed
docker service rollback service_name

DNS Configuration

# Verify DNS configuration
nslookup your-domain.com

GitHub Container Registry Setup

GitHub Actions Workflow

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 Docker Stack Configuration

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

Version Management and Rollbacks

# 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

Additional Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment