All projects / Project overview

Production habits,
one VPS.

Everything I publish runs on a single hardened virtual server I operate the way I ran datacenters: one public entry point, secrets delivered at boot rather than stored, containers with the least privilege they can work with, and every change reviewed and tested before it ships. This page is the live infrastructure behind the site you are reading.

Rocky Linux 10Docker Composenginx edge proxyGitLab CE + runnerOpenBaoTailscale

AI contribution: AI-assisted runbooks and compose hardening; architecture and access decisions are mine. How I work with AI

01 / WHAT RUNS HERE

Small estate,
real boundaries.

The same questions an assessor asks of a datacenter apply to one box: who can reach it, where the secrets are, what a compromised container can do, and how a bad change gets undone.

EDGE PROXY

One public door

An unprivileged nginx container owns ports 80 and 443. It passes GitLab's TLS through by SNI so GitLab keeps its own certificate, terminates TLS for this site, and redirects plain HTTP. Let's Encrypt renewal runs from a systemd timer through the same proxy's webroot.

SOURCE CONTROL

GitLab CE and a runner

Every repository lives on a private GitLab on the VPS: projects private by default, push-to-create off, one write-enabled deploy key per project per agent so an AI coding agent can only touch what it was granted.

SECRETS

OpenBao with AppRole

The site's chat API key never lives in git or in an image. A systemd unit authenticates with an AppRole at boot, renders the key into a root-owned tmpfs file, and the container reads it as an env layer. If the vault is down the site still serves; only the chat returns a friendly 503.

THIS WEBSITE

Hardened by default

The Next.js standalone image runs as a non-root user with a read-only root filesystem, every Linux capability dropped, no-new-privileges, tmpfs for caches, a health check, and bounded JSON logs. The same compose house style applies to the other services.

ADMIN ACCESS

Tailscale only

SSH and Git over SSH are reachable only on the tailnet; the public hostname refuses the Git port. Internal services bind to loopback or the tailnet address, never 0.0.0.0.

AUTOMATION

Scheduled and reviewed

n8n handles scheduled jobs, an hourly sync reconciles the production checkout with GitLab, and every change to the site goes through a merge request and a test gate before it is built.

02 / HOW A CHANGE SHIPS

Review, gate,
build, verify.

The deploy is deliberately manual and deliberately boring. The test gate on the host is the same one CI runs, so a merge that passed review cannot reach production without passing again on the machine that serves it.

  1. STEP 01

    Open a merge request

    Changes land on main only through a reviewed merge request on the private GitLab, including the ones AI agents author.

    git push origin claude/site-improvements
  2. STEP 02

    Run the gate on the host

    The production checkout pulls main and runs the full suite: a build, rendered-HTML assertions for every route, a private-data exclusion check, and a 105 KB gzip budget for critical assets.

    npm ci && npm test
  3. STEP 03

    Build and replace the container

    Compose builds the image from the repo Dockerfile and swaps the container; the health check gates traffic.

    docker compose -f deploy/compose.yml up -d --build
  4. STEP 04

    Verify, or roll back

    A deep link and the chat are checked by hand. Rollback is a redeploy of the previous commit; there is no server state to migrate.

    git checkout <previous-sha> && docker compose -f deploy/compose.yml up -d --build
03 / SAFEGUARDS

Fail safe,
not fail open.

Each guard was added after thinking about how the thing it protects would actually be abused or forgotten.

CHAT SPENDING

A paid endpoint with a hard ceiling

The public chat reserves its cost in SQLite before every upstream call: a monthly and daily allowance, six admitted requests per minute, two concurrent calls, reservations priced at twice the published rate and never refunded. Exhaustion returns a 503 with the reset time instead of a bill.

NO TRACKING

Nothing to leak

No analytics, no cookies, no account system, no third-party scripts. The browser gets a Content-Security-Policy that allows only this origin.

CERTIFICATES

Renewed without hands

A systemd timer renews the Let's Encrypt certificate through the proxy's ACME webroot and reloads nginx; the GitLab certificate has its own path that the proxy never touches.

BLAST RADIUS

Keys scoped to one job

Each coding agent has its own deploy key enabled per project, never a personal access token. Any token that appears in a transcript is rotated.

STATUSRocky Linux 1011 containersPublic ports: 80 and 443 onlyAdmin: TailscaleRuns this site
04 / GET IN TOUCH

Want the runbooks?

The compose files, proxy configuration, secret-delivery unit and deployment guide live in the site's private repository. Happy to walk through any of it, or to talk about how these habits scale to a team.

Email me about the platform