Home / Development / Self-hosting a static site on a VPS

Self-hosting a static site on a VPS

Oct 18, 2026Noteastro · caddy · vps · github-actions · systemd

This site is written in Markdown, built with Astro, and served from a small Ubuntu VPS. Publishing is a git push; the site updates about two minutes later.

 you ── git push ──▶ main ──▶ GitHub Actions: npm run build
                                     │
                                     ▼
                         deploy branch (built HTML only)
                                     ▲
                                     │ fetch every 2 min
                                     │ (read-only deploy key)
 browser ──▶ Caddy :443 ─────────────┴──▶ /var/www/loneelixir
            auto HTTPS

1. Point the domain at the server

Two DNS records: an A record for the bare domain pointing at the VPS IP, and www as either another A record or a CNAME to the bare domain. Check with any DNS lookup before going further, because the certificate step needs it.

2. Build in CI, publish to a branch

A GitHub Actions workflow runs npm ci && npm run build on every push to main and commits the dist/ folder to a separate deploy branch. The server never needs Node, and the source never sits in the web root.

3. Serve it with Caddy

Caddy gets and renews Let’s Encrypt certificates on its own as soon as a site block names a real domain:

loneelixir.cloud {
    root * /var/www/loneelixir
    @hidden path /.* /.*/* /deploy/* *.md
    respond @hidden 404
    encode gzip
    file_server
}

www.loneelixir.cloud {
    redir https://loneelixir.cloud{uri} permanent
}

The @hidden matcher matters: the web root is a git checkout, so without it /.git/ would be downloadable.

4. Give the server read-only access

A deploy key is an SSH key attached to one repository instead of your account. Generate it on the server and add the public half under Settings → Deploy keys with write access off. If the server is ever compromised, the key can read this one repo and nothing else.

ssh-keygen -t ed25519 -f ~/.ssh/site_deploy -N "" -C "vps-deploy"

# ~/.ssh/config
Host github.com
  IdentityFile ~/.ssh/site_deploy
  IdentitiesOnly yes

5. Fetch on a timer

A systemd timer fetches the deploy branch every two minutes and resets the checkout to it. Because CI rewrites that branch from scratch, a reset is more reliable than a pull.

# /etc/systemd/system/site-pull.service
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'git -C /var/www/site fetch -q origin deploy && git -C /var/www/site reset -q --hard origin/deploy && git -C /var/www/site clean -qfdx'

# /etc/systemd/system/site-pull.timer
[Timer]
OnBootSec=1min
OnUnitActiveSec=2min
[Install]
WantedBy=timers.target

Why pull instead of push

The usual alternative is a workflow that SSHes into the server on every push. It deploys in seconds instead of minutes, but GitHub then holds a key that can log into your server. For a static site, a two-minute delay is a fair trade for GitHub never having that access.