Small teams often copy files over FTP or SSH and hope for the best. On the other side, Kubernetes is too much for a handful of apps. There is a middle path that gives you most of the benefits: every change is built, tested and deployed the same way, and every deployment can be rolled back.
The building blocks
- Git as the single source of truth, with a protected main branch.
- Docker images, so what runs in production is exactly what was tested.
- GitHub Actions to build, test and push the image to the GitHub Container Registry.
- Docker Compose on a hardened Ubuntu VPS behind a reverse proxy (Nginx or Caddy) and Cloudflare.
The workflow
name: deploy
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- run: |
docker build -t ghcr.io/acme/app:${{ github.sha }} .
docker push ghcr.io/acme/app:${{ github.sha }}
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: deploy
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /srv/app
TAG=${{ github.sha }} docker compose up -d
docker image prune -f
Why tag images with the commit SHA
Each image is tied to an exact commit. Rolling back is running the same compose command with the previous SHA, which takes seconds. The latest tag cannot give you that.
Health checks and zero surprises
Add a healthcheck to every service in the compose file and make the deploy step fail if the container is not healthy after a short wait. Keep environment files on the server, owned by the deploy user, and never in the repository.
Approvals for production
GitHub Environments let you require a reviewer before the production job runs, while staging deploys automatically. That one setting prevents most "who deployed this on a Friday?" incidents.
This setup runs comfortably on a single small VPS, costs almost nothing and gives a small team the same discipline that large teams get from much heavier tooling.