A Practical DevOps Pipeline for Small Teams: GitHub Actions, Docker and One VPS

Mena Masoud
Mena Masoud
22 Feb 20262 min read
Share

You do not need Kubernetes to deploy safely. One VPS, Docker Compose and a GitHub Actions workflow give a small team repeatable, reversible deployments.

A Practical DevOps Pipeline for Small Teams: GitHub Actions, Docker and One VPS

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.

Need help with this?

I help companies put these ideas into production. Tell me about your project.

Get in touch