Skip to main content
.nigelsmith

Projects

Infrastructure I run, websites I've shipped, and a few games and tools in progress.

One host · four repos · zero manual steps

Shipping a Go fleet to a box I can throw away

Four Go services live behind one SSH port on a single EC2 instance. Merging to main builds the image, ships it, and restarts the service on the host itself — and every save is streamed to S3 so the instance underneath is replaceable.

$ git push origin main

  1. 01

    Commit

    • task branch → PR
    • never main

    main deploys for real.

  2. 02

    Test

    ubuntu-latest

    • go vet ./...
    • go build ./...
    • go test -race ./...

    Runs again on release. Never skipped.

  3. 03

    Publish

    ubuntu-latest

    • ghcr.io/…:latest
    • :sha-b8315db

    Auth is the run's own token. Nothing stored.

  4. 04

    Deploy

    [self-hosted, production]

    • docker compose pull
    • docker compose up -d

    Runs on the box, not over SSH.

  5. 05

    Live

    • ● ONLINE
    • ~15s

    Prober confirms the new version.

The delivery path, in order: Commit, then Test, then Publish, then Deploy, then Live. Each stage lists the commands and artifacts it produces.

Why the runner lives on the host

CI opens no inbound port

The host's real sshd is locked to a single IP, and GitHub-hosted runners come from an ever-changing range that will never match it — a dial-in deploy job would just time out. Registering the runner as a service on the box instead means the deploy reaches production without anything having to be let in.

Durability

The instance is disposable

The volume is a cache; the bucket is the fleet's data. Credentials come from the EC2 instance role, so no AWS key exists in any image or compose file. Drilled locally against MinIO — a hard kill plus a volume delete restored clean, with a measured 12s RPO against an enforced 60s budget.

Those drills prove the mechanism. Only the live host proves the credentials — an instance-loss drill is still on the list.

Topology

One public port, everything else private

The router terminates the player's SSH, then opens a second connection to the chosen game on a private bridge network. Games publish nothing, so the router is the only public listener and the only place rate limiting has to happen. A game that is down degrades to OFFLINE in the menu instead of breaking the arcade.
$0/mo
Flat hosting cost, whatever the game count
0s
Measured RPO in a kill-drill (60s budget)
0
Inbound ports opened for CI
0
Tests across four repos, ~42% of the code

Portfolio Shell

guest@portfolio: ~
Nigel's portfolio Type `help` for commands. System time: Tue, Aug 25, 05:02:56
guest@portfolio:~$

Featured Web Applications

portfolio.log
[1652][INFO]Portfolio booted. No production servers were harmed.
[1653][DEBUG]Found another side project. Opened a new tab.
[1654][INFO]Photos optimized. Film grain left intact.
[1655][WARN]README is three commits behind the code.
[1656][INFO]Build passed. Suspicious, but acceptable.
[1657][ERROR]One more idea added to the backlog.

Interactive

Small games and tools I build for the browser.