Appearance
One box, done well
At 2:13 a.m., an agent turn stalls while Postgres is writing and the model provider is timing out. The useful question is not whether the platform has enough services. It is whether one operator can tell what failed, restart the right process, and preserve the conversation.
A single machine can answer that question well. For a private assistant or a trusted small group, one server removes network hops, cross-zone failures, and a large amount of deployment machinery. It also concentrates failure. The honest design accepts both facts.
Concept map
text
Internet
|
HTTPS edge
|
+-- static web
+-- authenticated API and WebSocket routes
|
+-- agent worker
+-- sync cache
+-- Postgres, authoritative state
+-- shared-state Redis, authoritative and backed up
+-- analytics/cache Redis, disposable or reconstructible
Off-box: image registry, secret provider, backups, telemetryThe box owns execution. Other systems hold the things needed to rebuild it.
Terms used in this chapter
Authoritative state is data whose loss changes what users know or have done. Conversations and schedules usually belong here.
Rebuildable state improves speed but can be recreated from authoritative data. A replication cache is a common example.
Control plane means deployment, secret delivery, backups, and monitoring. These functions should still work when the application process is unhealthy.
Failure domain is the set of components one fault can take down. On one server, the host, disk, network path, and every container share a failure domain.
Draw the boundary before choosing sizes
Start with exposure. Publish only the HTTPS edge. Keep the database, cache, agent worker, metrics endpoints, and internal WebSocket services on the container network. A private port is easier to defend than a public port with another password.
Caddy is a good fit for this job because it handles certificates, static files, reverse proxying, and authentication delegation in a small configuration. Nginx, Traefik, or a managed load balancer can enforce the same boundary. The transferable rule is that one component owns public ingress and every internal route has a stated authentication rule.
A compact Compose shape looks like this:
yaml
services:
edge:
ports: ["80:80", "443:443"]
worker:
expose: ["3000"]
database:
expose: ["5432"]
volumes: ["db-data:/var/lib/postgresql/data"]expose documents an internal port. It does not publish that port on the host.
Resource sizing comes after topology. Two virtual CPUs and four gigabytes of memory can support a personal agent system if model inference happens elsewhere and container limits prevent optional collectors from starving Postgres. Watch actual memory pressure, swap activity, disk growth, and turn concurrency. Upgrade from evidence, not from the number of service names.
State needs names, not assumptions
Classify every volume and table. Postgres is authoritative. A Zero or SQLite replica is rebuildable. TLS state is replaceable, though preserving it avoids needless certificate churn. Redis may be authoritative for some features and disposable for others, so one Redis label is not enough.
This classification controls backups. A compressed logical Postgres dump is portable and easy to inspect. It should leave the machine through an encrypted remote such as object storage reached by rclone. A backup left on the same disk protects against an accidental row deletion, but not against disk or account loss.
Run restore drills on a disposable host. A successful backup command proves only that a file was written. A restore drill proves the file is readable, credentials are available, service order is understood, and caches can rebuild.
Make ordinary operations boring
The host should run immutable application images. It should not need the source repository, Node.js, or a compiler. A deployment pulls images, takes a backup, runs migrations once, replaces containers, checks health, and records the active image digests.
Health checks need layers. A process check catches a crash. A database query catches broken dependencies. A public request catches DNS, certificate, edge routing, and authentication regressions. One endpoint cannot stand in for all three.
For example, an operator can verify the public boundary without signing in:
sh
curl -fsSI https://chat.example.net/ |
awk 'NR == 1 || tolower($0) ~ /^location:/'If the expected result is a redirect to a login page, record that exact contract. "The site loads" is too vague for automation.
Decisions and rejected paths
Choose one machine when the users share a trust boundary, the data fits comfortably on one disk, short restoration downtime is acceptable, and one operator can understand the whole system.
Reject Kubernetes at this stage. It would add a scheduler, networking model, storage classes, and another credential system without removing the single-database dependency. Containers are still useful because they package processes and make dependencies explicit. Compose is enough to supervise a small fixed graph.
Reject an on-box monitoring stack when it competes with the application and disappears during a host failure. An outbound collector feeding Grafana Cloud is the explicit vendor choice in the reference system. A second Prometheus and Grafana host is a transferable alternative.
Reject public database and metrics ports. SSH tunnels or container-local commands cover maintenance. Public listeners increase the number of credentials and patch paths that must remain correct.
Where this design breaks
Disk exhaustion can stop Postgres and prevent logs or backups from being written. Alert before usage reaches the emergency threshold.
A stalled replication consumer can retain write-ahead logs until the disk fills. Bound WAL retention and accept that a disposable replica may need rebuilding.
An agent worker can be healthy at the HTTP level while its outbox is not moving. Measure queue age and terminal errors, not only process uptime.
The Docker administrator can inspect container environments and volumes. A read-only application filesystem does not reduce that host-level trust.
Finally, one box is not high availability. Provider backups and off-site dumps shorten recovery, but a dead VM still causes downtime.
Field checklist
Previous: Chapter 30, "Launch is a different mode".
Next chapter
Chapter 32, "Hetzner from zero to cell", turns this boundary into a practical host setup.