veasy

guide · DevOps

Deploy a static site to your VPS with Docker and GitHub Actions

A production-grade pipeline for any static site: build in CI, package with nginx, ship to your own server with one git push.

You do not need a platform subscription to get push-to-deploy. With a $5 VPS, Docker and GitHub Actions you get the same workflow, fully under your control. This is the exact setup behind veasy.vn.

What you need

  • A VPS (Ubuntu 22.04+) with Docker installed
  • A GitHub repository for your site
  • SSH key access to the VPS

1. Package the site with nginx

A multi-stage Dockerfile keeps the final image tiny:

1FROM node:22-alpine AS build
2WORKDIR /app
3COPY package*.json ./
4RUN npm ci
5COPY . .
6RUN npm run build
7
8FROM nginx:alpine
9COPY --from=build /app/dist /usr/share/nginx/html
10EXPOSE 80

If your site is plain HTML with no build step, skip the first stage and copy files straight into nginx.

2. Compose file on the VPS

1services:
2 web:
3 image: ghcr.io/YOUR_USER/YOUR_REPO:latest
4 restart: unless-stopped
5 ports:
6 - "8080:80"

Put your reverse proxy (nginx or Caddy) in front for TLS. Caddy gives you automatic HTTPS in two lines.

3. The GitHub Actions workflow

1name: Deploy
2on:
3 push:
4 branches: [main]
5
6jobs:
7 deploy:
8 runs-on: ubuntu-latest
9 permissions:
10 packages: write
11 contents: read
12 steps:
13 - uses: actions/checkout@v4
14 - uses: docker/login-action@v3
15 with:
16 registry: ghcr.io
17 username: ${{ github.actor }}
18 password: ${{ secrets.GITHUB_TOKEN }}
19 - uses: docker/build-push-action@v6
20 with:
21 context: .
22 push: true
23 tags: ghcr.io/${{ github.repository }}:latest
24 - name: Deploy on VPS
25 uses: appleboy/ssh-action@v1
26 with:
27 host: ${{ secrets.SSH_HOST }}
28 username: ${{ secrets.SSH_USER }}
29 key: ${{ secrets.SSH_PRIVATE_KEY }}
30 script: |
31 cd /opt/site
32 docker compose pull
33 docker compose up -d

4. Why this beats plain rsync

  • Rollback is one command: retag or docker compose up an older image.
  • The server stays clean: no node, no build tools on the VPS, just Docker.
  • It grows with you: need an API or analytics later? Add a container to the compose file. The pipeline does not change.

Common pitfalls

  • Private GHCR images need a login on the VPS too: docker login ghcr.io with a token that has read:packages.
  • Pin your host key in CI instead of disabling strict host checking.
  • Add a smoke test step that curls your domain after deploy; a green pipeline that serves a 502 is worse than a red one.
ShareFacebookLinkedInX

Comments