
From Zero to Production: My CI/CD Pipeline Setup
How I set up a repeatable, zero-downtime deployment pipeline for my Go + Next.js stack using GitHub Actions, Docker, and a $6/month VPS.
Web Performance Deep Dives
Part 2 of 3
Practical investigations into database and infrastructure performance for modern web stacks.
Table of Contents
Table of Contents (4 sections)
The Pipeline at a Glance

The entire pipeline β from git push to containers running in production β completes in under 3 minutes. It runs lint, type checking, Docker image building, a push to GitHub Container Registry (GHCR), and a rolling deployment to the VPS via SSH with zero downtime.
The GitHub Actions Workflow
The workflow is split into two jobs: ci (build, lint, test) and deploy (production release). The deploy job depends on ci and only runs on pushes to main β pull requests run ci only.
1name: CI/CD Pipeline23on:4 push:5 branches: [main]6 pull_request:7 branches: [main]89jobs:10 ci:11 runs-on: ubuntu-latest12 steps:13 - uses: actions/checkout@v41415 - name: Set up Go16 uses: actions/setup-go@v517 with:18 go-version: '1.22'1920 - name: Lint and vet21 run: |22 go vet ./...23 go build -o /dev/null ./...2425 - name: Build and push Docker image26 if: github.ref == 'refs/heads/main'27 run: |28 echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin29 docker build -t ghcr.io/${{ github.actor }}/myapp:${{ github.sha }} .30 docker tag ghcr.io/${{ github.actor }}/myapp:${{ github.sha }} ghcr.io/${{ github.actor }}/myapp:latest31 docker push ghcr.io/${{ github.actor }}/myapp:latest3233 deploy:34 needs: ci35 runs-on: ubuntu-latest36 if: github.ref == 'refs/heads/main'37 steps:38 - name: Deploy to VPS via SSH39 uses: appleboy/ssh-action@master40 with:41 host: ${{ secrets.VPS_HOST }}42 username: deploy43 key: ${{ secrets.VPS_SSH_KEY }}44 script: |45 docker pull ghcr.io/${{ github.actor }}/myapp:latest46 docker stop myapp || true && docker rm myapp || true47 docker run -d --name myapp --restart unless-stopped \48 -p 8080:8080 --env-file /opt/myapp/.env \49 ghcr.io/${{ github.actor }}/myapp:latestGitHub Secrets Setup
Go to your repo Settings β Secrets β Actions and add VPS_HOST (your server IP) and VPS_SSH_KEY (private key content). GITHUB_TOKEN is injected automatically β no manual secret needed for GHCR pushes.
Never Deploy as Root
Create a dedicated deploy user on your VPS with SSH key auth only and no sudo rights. Grant it access only to the Docker socket and the /opt/myapp directory. Deploying as root is a security liability that many tutorials silently skip.
The Deploy Script
1#!/bin/bash2set -euo pipefail34APP_DIR="/opt/myapp"5IMAGE="ghcr.io/lordofthemind/myapp:latest"6CONTAINER="myapp"78echo "[deploy] Pulling latest image..."9docker pull "$IMAGE"1011echo "[deploy] Stopping previous container..."12docker stop "$CONTAINER" 2>/dev/null || true13docker rm "$CONTAINER" 2>/dev/null || true1415echo "[deploy] Starting new container..."16docker run -d \17 --name "$CONTAINER" \18 --restart unless-stopped \19 -p 8080:8080 \20 --env-file "$APP_DIR/.env" \21 "$IMAGE"2223echo "[deploy] Verifying health..."24sleep 325curl -sf http://localhost:8080/api/health || { echo "[deploy] Health check failed!"; exit 1; }26echo "[deploy] Done."YouTube Video
External content
Pre-Deploy Checklist
- All CI checks pass (lint, type check, build)
- Docker image builds locally without errors
- VPS_HOST and VPS_SSH_KEY secrets are set in GitHub
- VPS has sufficient disk space (run docker system prune if needed)
- Database migrations run before container restart
- /api/health returns 200 after deployment
For a battle-tested CLI tool that wraps all of this into a single command with rollback support and Slack notifications, see the go-deploy-cli in the related tools section.
Related Content
Explore related articles, projects, and tools.