main: merge, and a few minutes later everything is running that commit.
What runs where
The DNS zone is on Cloudflare. The site’s records are created by Cloudflare when the Worker’s custom domains are attached;
api and rpc are created by the workflow (deploy/dns.mjs) as DNS-only records, so nginx on the VPS terminates TLS itself and sees real client addresses for rate limiting.
The site on Cloudflare
wrangler.jsonc deploys dist/ as static assets with no Worker script:
not_found_handling: single-page-application, so every path serves the app;- custom domains
istravex.comandwww.istravex.com, withworkers.devand preview URLs off; public/_headers(copied intodist/) sets HSTS,nosniff, a referrer policy,X-Frame-Options: DENY, and a one-year immutable cache on Vite’s fingerprinted/assets/*. Everything else revalidates on every request.
wrangler deploy --dry-run on every push, so a broken config fails before merge.
The VPS is shared
The VPS also runs other, unrelated sites behind its own nginx, so the deploy is careful to stay inside its own footprint:
Every nginx change is checked with
nginx -t before a graceful systemctl reload nginx; if the check fails, the previous istravex-api.conf is restored and nothing is reloaded.
The pipeline
.github/workflows/deploy.yml runs when CI completes successfully on main, or manually from the Actions tab. Its three jobs run in parallel where they can.
1
Site → Cloudflare
npm run build, validate with wrangler deploy --dry-run, then wrangler deploy --message <commit>. The first deploy attaches the custom domains, replacing any placeholder records at those names. A smoke test fetches https://istravex.com/.2
API image → GHCR
Built from the
Dockerfile and pushed to ghcr.io/hackcat29/istravex-api, tagged with the commit SHA and latest.3
API → VPS
Upserts the
api and rpc DNS records, then over SSH (strict host-key checking against a pinned known_hosts) copies deploy/ to /opt/istravex and runs deploy/remote.sh. That script logs in to GHCR with the workflow’s short-lived token, pulls the image for this commit, and runs docker compose up -d --wait, which only returns once the API’s health check passes. It then installs the nginx site. On the first deploy it serves the ACME challenge over HTTP, gets a certificate from certbot (webroot, renewed by the host’s certbot timer with a reload hook), and switches to HTTPS. A smoke test then requires https://api.istravex.com/v1/health to report the commit just deployed, and the RPC to report healthy.concurrency: deploy-production), so two merges in quick succession deploy in order. Each part is skipped, not failed, until it’s configured.
One-time setup
1
Cloudflare API token
In the Cloudflare dashboard, My Profile → API Tokens → Create Token, start from the Edit Cloudflare Workers template, add Zone → DNS → Edit, and limit it to your account and the
istravex.com zone. Store it in GitHub:npx wrangler whoami prints the account ID.2
VPS access
Generate a key just for deploys, authorise it on the server, and pin the host keys:Compare the scanned fingerprints (
ssh-keygen -lf known_hosts) with the server’s own (ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub, from the provider’s console) before trusting them.3
Repository secrets and variables
Then delete the private key from your machine.
4
Server prerequisites
Docker with the Compose plugin, nginx, certbot (with an ACME account registered), and ports 80 and 443 open. The deploy doesn’t install or reconfigure any of these, since other sites depend on them.
5
Docs domain
See Docs. In Cloudflare, keep the
docs CNAME DNS-only: Mintlify issues its own certificate.Operating it
Site.npx wrangler deployments list shows recent deploys; npx wrangler rollback returns to the previous one instantly.
API.