Deploying suffeffix.com on an IONOS VPS
The site is a fully static export: plain HTML, CSS, JS and downloadable data files. There is no application server, database or Python in production. The VPS only needs nginx to serve a folder, plus a free Let's Encrypt certificate.
GitHub push ─► CI builds the site ─► rsync over SSH ─► /var/www/suffeffix/releases/<timestamp>/
└► `current` symlink flips ─► nginx serves it
Each deploy goes into a fresh release directory and the current symlink flips in one step, so visitors never see a half-uploaded site. The last five releases stay on disk, so rollback is instant.
Status. The scripts are syntax-checked, and the release / prune / rollback logic was exercised locally against a fake server, but the nginx configuration andsetup-server.shhave not been run against a real server. Setup runsnginx -tbefore every reload, so a mistake cannot take a running site down, but expect to fix small things on the first real run.
What is on the server afterwards
| Path | Purpose |
|---|---|
/etc/nginx/sites-available/suffeffix.conf | the site (from deploy/nginx/suffeffix.conf) |
/etc/nginx/snippets/suffeffix-security-headers.conf | CSP, HSTS and the other security headers |
/var/www/suffeffix/releases/… | uploaded builds; current points at the live one |
/etc/letsencrypt/ | certificate, renewed automatically by certbot's systemd timer |
user deploy | key-only SSH user that owns the web root and has no sudo rights |
nginx handles: HTTP → HTTPS, www → suffeffix.com, TLS 1.2/1.3, gzip, immutable caching for hashed assets, open CORS and a one-hour cache for /data/ downloads, the custom 404 page, and no dotfiles.
One-time setup
Assumes Ubuntu 22.04 or 24.04 (or Debian) with root SSH access, which is what IONOS provisions by default.
1. Point the domain at the server (IONOS DNS)
IONOS → Domains & SSL → your domain → DNS:
Arecord, host@(the bare domain) → the VPS's IPv4 address.Arecord, hostwww→ the same IPv4 address (or aCNAMEforwww→suffeffix.com).- Add
AAAArecords with the VPS's IPv6 address only if the server really has working IPv6 — a wrongAAAA
record sends IPv6 visitors to a dead end.
- Delete any default parking or "website builder"
A/AAAArecords IONOS created for the domain.
Check propagation before continuing: nslookup suffeffix.com and nslookup www.suffeffix.com should both show your IP.
2. Open the ports in the IONOS firewall
In the IONOS Cloud Panel → your server → Network → Firewall Policies, make sure the policy attached to the server allows inbound 22 (SSH), 80 (HTTP) and 443 (HTTPS). The setup script also enables the server's own ufw firewall with the same three ports, but a restrictive Cloud Panel policy would block traffic before it reaches the server.
3. Make a deploy key (on your computer)
ssh-keygen -t ed25519 -f ~/.ssh/suffeffix_deploy -C suffeffix-deploy # no passphrase if CI will use it
Keep the private key (suffeffix_deploy) secret; the public key (suffeffix_deploy.pub) goes to the server.
4. Provision the server
ssh root@YOUR_SERVER_IP git clone https://github.com/VSSK007/suffeffix.git && cd suffeffix sudo DEPLOY_PUBKEY="ssh-ed25519 AAAA... suffeffix-deploy" CERT_EMAIL="you@example.com" bash deploy/setup-server.sh
Replace the DEPLOY_PUBKEY value with the single line inside suffeffix_deploy.pub.
Server already hosts other sites? Add SHARED_SERVER=1 to the command (before bash). The script then aborts unless nginx is running and passes nginx -t, does not touch ufw, keeps nginx's default site, and writes only suffeffix's own files (sites-available/suffeffix.conf, its headers snippet, /var/www/suffeffix, /var/www/certbot, the deploy user). Make sure ports 22, 80 and 443 are already open. Before running it, check what owns the web ports with ss -tlnp | grep -E ':(80|443)'; if it isn't nginx, do not run the script.
The script installs nginx, certbot, ufw and rsync; creates the deploy user; sets up the web root; opens the firewall; obtains the certificate with an HTTP challenge; installs the final HTTPS config and a renewal hook. It is safe to re-run.
5. Trust the server's host key, and test the login
From your computer (this also records the server's key so later connections cannot be silently redirected):
ssh -i ~/.ssh/suffeffix_deploy deploy@YOUR_SERVER_IP # answer "yes", then `exit` ssh-keyscan YOUR_SERVER_IP # copy this output for step 6
On Windows, the bundled ssh-keyscan can fail with unsupported KEX method. Get the host key from the server instead, over a login you already trust.
Log in as deploy, then run this one line on the server (the host-key file is public):
echo "YOUR_SERVER_IP $(cut -d' ' -f1,2 /etc/ssh/ssh_host_ed25519_key.pub)"
It prints one line (YOUR_SERVER_IP ssh-ed25519 AAAA…); use it as VPS_KNOWN_HOSTS, then exit (do not type yes at the shell prompt: it runs a command that prints y forever). In PowerShell, write ~/.ssh/ as $env:USERPROFILE\.ssh\ and create the folder first with New-Item -ItemType Directory -Force.
Deploying
Automatic (recommended): GitHub Actions
.github/workflows/deploy-vps.yml validates the dataset, builds the site, checks every link and uploads it to the server on every push to main. In GitHub → repository → Settings → Secrets and variables → Actions, add:
| Secret | Value |
|---|---|
VPS_HOST | the server's IP address or hostname |
VPS_SSH_KEY | the contents of the private key suffeffix_deploy |
VPS_KNOWN_HOSTS | the output of ssh-keyscan YOUR_SERVER_IP from step 5 |
VPS_USER (optional) | defaults to deploy |
VPS_PORT (optional) | defaults to 22 |
Until VPS_HOST and VPS_SSH_KEY exist the deploy step is skipped rather than failed. That means a green run does not prove a deploy happened: open the run and check that the Deploy over SSH step is not marked skipped. Then push to main, or run the deploy-vps workflow by hand from the Actions tab.
Manual
Uses rsync when it is installed and otherwise streams the files with tar over ssh, so it also works from Git Bash on Windows with no extra software. Point it at your private key with SSH_KEY:
make deploy HOST=deploy@YOUR_SERVER_IP # builds, uploads, flips the symlink, checks the site # or, without make, after `make site` has built the site: SSH_KEY=~/.ssh/suffeffix_deploy bash deploy/deploy.sh deploy@YOUR_SERVER_IP
Roll back
Switches current to the previous release; needs only ssh:
bash deploy/deploy.sh deploy@YOUR_SERVER_IP rollback
After the first deploy
curl -I https://suffeffix.com/ # 200, with Strict-Transport-Security and Content-Security-Policy curl -I http://suffeffix.com/ # 301 to https://suffeffix.com/ curl -I https://www.suffeffix.com/ # 301 to https://suffeffix.com/ curl -I https://suffeffix.com/data/suffeffix-v0.2.0.zip # 200, Access-Control-Allow-Origin: * curl -I https://suffeffix.com/nope/ # 404
Then:
- On the server:
sudo certbot renew --dry-run(renewal works) andsystemctl list-timers | grep certbot(it is scheduled). - Submit
https://suffeffix.com/sitemap.xmlin Google Search Console and Bing Webmaster Tools. - Run Lighthouse against production and record the scores in
docs/LAUNCH_CHECKLIST.md. - Once key login works, turn off SSH password logins: set
PasswordAuthentication noin/etc/ssh/sshd_config, then
sudo systemctl reload ssh. Keep a second terminal open while you do it, in case you lock yourself out.
- Consider
sudo apt install unattended-upgradesso security updates install themselves, and a free uptime monitor.
Build locally
pip install "pydantic>=2.7,<3" fastapi uvicorn httpx # once cd apps/web && pnpm install && cd ../.. # once make site # export data, build, check links make preview # http://localhost:3000, behaves like the nginx config
make site fails if the dataset does not validate, if the family constraint is violated, or if any internal link or #anchor is broken.
Updating content
Data lives in data/*.json. After editing: make validate && make test && make site, then push. CI re-validates and the workflow deploys. The dataset release under /data/ is rebuilt from the same files on every build, with fresh checksums, so the page can never list a file that is not downloadable.
Other hosts
Nothing here is specific to IONOS. Any static host works if you upload apps/web/out/, serve directories with a trailing slash (/lexicon/, which is what every canonical URL uses), serve 404.html for missing paths, and send the headers in deploy/nginx/security-headers.conf. Netlify, Vercel, Cloudflare Pages and GitHub Pages are all fine; this repository no longer ships configuration for them.