
A real build log. Every command in this article was actually run. Everything that broke is in here too.
This site runs on a second-hand server in a home office. It was designed, coded, tested and deployed by AI agents working under human direction - the human decided what to build and why; the agents did the legwork. Here is how the whole thing fits together, so you can build something similar.
Why this belongs on this site: this workflow - describe what you want, let an agent produce the first draft, then review and steer - removes the exact steps that make some of us slowest (staring at a blank file, typing boilerplate, hunting typos) and keeps the steps we're good at (judging whether the thing is right).
The stack, in one picture
One docker-compose.yml, four containers:
- App - Next.js (App Router). One codebase does the frontend, the API, and every database write through server actions. One place to look when something mutates data.
- Database - Postgres, with Prisma as the schema/migration layer.
- MinIO - S3-compatible object storage for uploads (avatars, images). It is never exposed to the internet; the app proxies files back out through its own API route.
- Caddy - the front door. Locally it serves plain HTTP on one port. In production you set one variable (
SITE_ADDRESS=yourdomain.com) and Caddy gets HTTPS certificates from Let's Encrypt automatically. No certbot, no cron, no drama.
The whole thing deploys with one command:
cd /path/to/site
docker compose up -d --build
That is the entire deploy procedure. Rebuilds the image, applies database migrations on container start, restarts what changed.
Decisions worth stealing
Hand-rolled auth, on purpose. No auth provider, no framework plugin. Email + password with bcrypt, and a random session token in an httpOnly cookie - the database stores only a SHA-256 hash of that token, so a leaked database doesn't leak sessions. This is less code than integrating a provider, and every line of it is readable in one sitting. For a small community site, fewer moving parts beats more features.
Object storage behind the app, not beside it. Uploads go app → MinIO over the S3 API; downloads come back through an app route. MinIO never needs a public port or its own TLS. One less thing on the attack surface.
Migrations apply themselves. The container entrypoint runs prisma migrate deploy on start. You create migrations in dev, commit them, and deployment is just the deploy command above. Nobody ever SSHes in to "quickly run the migration".
Accessibility is a feature, not a checklist. The display controls (font, spacing, contrast) are the flagship feature of the site, persisted per-account and per-guest-cookie. When access needs drive the design, the testing follows: our end-to-end suite checks skip-link keyboard navigation the same way it checks login.
Testing: the part most homelab projects skip
The site has an end-to-end journey script (Playwright, headless Chromium) that acts like a real user: registers, changes display settings, posts a thread, replies, uploads an avatar, files a report, checks the admin can dismiss it, and tabs through the skip-link. About forty checks, run before every deploy:
NODE_PATH=$PWD/node_modules node docs/e2e.js
Two real bugs this caught that a human clicking around missed:
- The generic submit-button trap. A test that clicks
button[type=submit]will, for signed-in users, click the header's "Sign out" button - it's the first submit on every page. Selectors must target role + accessible name. Cost us 20 minutes; the fix made every future test more robust. - The copied-shim trap. Prisma's CLI shim breaks when you copy
node_modules/.bin/prismainto a slim Docker image, because it resolves paths relative to the real file. Callnode node_modules/prisma/build/index.jsinstead, and ship the full productionnode_modulesfrom a dedicated build stage rather than cherry-picking packages. Cherry-picking a modern CLI's dependency tree is a losing game.
Backups, because we mean it
Before any big change, and nightly:
docker compose exec db pg_dump -U <dbuser> <dbname> > backup-$(date +%F).sql
A backup you have never restored is a hope, not a backup - restore into a scratch database once and check a row count.
Where the AI actually fits
Three honest roles, none of them magic:
- Build agent. Given a specification ("display settings must persist for guests too"), it writes the code, runs the e2e suite, and reports what passed. The human reviews diffs, not blank pages.
- Image generation. A local GPU (an ex-datacentre card) runs an image model that produced the site's illustrations. No stock-photo licensing questions, and a consistent style across the site.
- Adversarial review. A different model audits what the first one built. Our security pass found and fixed real issues before launch. Two models disagreeing is a feature: it surfaces the assumptions.
The pattern that matters: small verified steps. Every change lands with the test suite green and a one-paragraph changelog entry. That log is how a foggy-brain day is no obstacle - you never need to remember what state the system is in, because the system tells you.
Start here if you want to copy this
- One
docker-compose.yml: app + Postgres + Caddy. Add object storage only when you need uploads. - Write the e2e journey script in week one, while the app is small. Growing it is easy; retrofitting it is misery.
- Automate the deploy to a single command before you automate anything else.
- Put the backup command in cron the same day you first have data you'd miss.
- Keep a changelog with timestamps. It is the cheapest observability tool ever invented.
Total infrastructure cost: an old server drawing about 126 watts. The expensive part was already in the room: knowing what good looks like.
Tried it? Improved it?
Tell the forum what worked and what didn’t: real experience beats recommendations, and the best answers get folded back into this guide with credit.
Related guides

Launch day, with a brain that rehearses disasters
The site went public today. This is the honest half of the story: the graveyard of finished-but-unshipped projects, fear dressed as engineering, and how making every disaster boring did what courage never could.
6 min read

What you actually buy when you buy a domain name
This week the site got its name. A plain-language tour of domains and DNS: the internet's phone book, why you rent a name rather than own it, what the dots mean, and why the name is worth more than the machine.
6 min read