Self-hosting a static site on a VPS
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.