The Box on My Desk Was Better Than the One I Was Renting
i own a 3090, a 5800X3D, 64gb of ram, and north of twenty terabytes of assorted drives. it sits on my desk. and for a while there i was paying Render every month for compute that, next to the machine i already owned, felt starved.
that gap is the whole story. not the version you might expect, though, where the box on the desk becomes the server. that version was never on the table: the box is my daily driver, the machine i actually live on all day, and it sits behind residential internet that makes no promises, either one disqualifying on its own. so it never leaves my desk. it is the yardstick: sit in front of real cores and real memory every day and you know exactly what starved feels like, and you stop being able to unfeel it.
the setup
TALON was my first real full-stack application. before it, everything i shipped was static or came out of a static site generator: no backend, no database, no persistent state to reason about. TALON changed the class of the problem: a geospatial land-intelligence platform, PostGIS parcels, lidar pipelines, ml inference, materialized views that actually materialize something.
Render was the correct first move
this is not a cloud-bad, metal-good post. Render was exactly right for where i was. it stripped away the ops burden so i could learn one hard thing at a time: my first production deploy, my first managed database, the first time i had to actually think about persistent state and migrations instead of just pushing static files. if i had tried to learn full-stack deployment and infrastructure in the same week i would have learned neither. Render bought me the room to level up in public.
why i outgrew it
the honest reason is that my workload is weird. geospatial plus ml is heavy, bursty, and memory-hungry, and managed paas prices exactly that shape of load punitively.
the pinch came from two directions at once. the first was compute: the live geospatial features, downloading lidar tiles, clipping them to a parcel, running the tpi and hag terrain analysis, were painfully slow on half-to-one cpu and starved memory, and even ordinary gis work dragged. the second was storage: the database was growing fast, and Render meters that growth, another monthly charge every 10gb. i could watch the curve and see exactly where it was going, and it was not going anywhere good.
here is the arithmetic that ended it. on Render i was paying about $33 a month for the database and roughly $23 each for the frontend and backend services. call it $79 a month, for three slices of half-to-one cpu each, memory measured in crumbs, and storage billed by the increment. the IONOS dedicated server is $128 a month. that is $49 more, for an entire machine, real cores, real memory, disk that treats a lidar tile like a file instead of a threat.
the point is not the $49. the point is the curve: on managed paas, capacity for my kind of workload scales in expensive little steps that were never shaped for it. on metal, one step bought more headroom than TALON can currently use.
deciding was the cheap part, though. the migration is what the pipeline had to survive.
the on-ramp: ci/cd and gating
the deploy pipeline is just quality gates in a different costume. same instinct as the validation gates i put between components in a data pipeline: nothing ships past a stage it has not earned. TALON flows feature/* into develop into staging into main, and the ci runs linting, security scanning, and the test suite against a live PostGIS instance before anything moves. by the time the migration happened, the pipeline was already the boring, trustworthy part. and it stayed boring: the two fights this migration picked were both about packets, neither one happened anywhere near the pipeline. a migration where the deploy machinery is the part you never have to think about is the whole point of building the gates before you need them.
the shape of the thing
single IONOS bare-metal box. one Proxmox host, not a cluster. the layout is three guests behind a nat gateway:
graph TD
I[internet] -->|80 / 443 only| H[Proxmox host<br/>NAT + iptables]
H --> B[bouncer, LXC<br/>Nginx Proxy Manager<br/>TLS + routing]
B -->|proxy_pass| A[app VM<br/>Dokploy: prod + staging stacks]
A -->|Postgres, internal only| D[db VM<br/>PostgreSQL + PostGIS]
- the bouncer is an lxc container, not a vm. it is a tiny stateless router, Nginx Proxy Manager doing tls termination and domain routing and nothing else. paying a whole kernel for that would be ceremony.
- the database is a full vm. Postgres 17 with PostGIS, on its own kernel with its own memory ceiling, and isolated on purpose:
pg_hbaonly accepts connections from the app vm, and nothing public routes to it at all. - the app server is a full vm running Dokploy, which is the part that makes me smile: Dokploy is a self-hosted paas. i did not actually leave paas. i took custody of it. push to GitHub, Dokploy pulls over a read-only deploy key, builds, and deploys, prod and staging stacks side by side on different internal ports. the ergonomics i was paying Render for, running on metal i control.
- the admin surfaces are deliberately not on the internet. Dokployâs dashboard and the proxy managerâs admin panel are reachable only through tunnels. as far as the public internet is concerned, this machine is a web server on 80 and 443 and nothing else.
two fights the migration docs remember
every migration has a couple of problems that earn a permanent place in the documentation. mine were both about packets that never arrived.
the hairpin. the inbound port-forwarding rules only matched traffic arriving on the public network card. so when the Astro frontend, rendering server-side inside the network, called our own public api hostname, the packet hit the host from the wrong side and got an instant reset. the symptom was maddeningly specific: the app loads, login works, and then every ssr-guarded route quietly bounces you back to the login page, because the frontendâs server-side fetch to its own api can not connect. the fix is nat reflection, extra rules that catch internal traffic aimed at the public ip and bend it back to the proxy. the diagnostic that cracked it was curl from inside a vm: a âcouldnât connectâ that returns in zero milliseconds means the packet never left the building.
the invisible firewall. IONOS runs a per-ip firewall policy upstream of the server, in front of the network card. which means your on-host rules can be letter-perfect and inbound traffic still dies before the machine ever sees it. the tell was tcpdump showing no inbound syn at all, just promiscuous-mode noise addressed to the neighbors. the rule i took away: when the packet never arrives, stop debugging the box and start debugging the building it lives in.
the small disciplines
- a daily datacenter backup job keeps the last five backups of every guest, and manual snapshots happen at milestones with names a future me can read. snapshots are for quick rollbacks during config changes; the daily job is the actual disaster recovery. they are not the same thing, and confusing them is how people lose weekends.
- the vmsâ dns config is immutable-locked at the filesystem level, the kind of small paranoid touch you develop the first time a reboot silently rewrites a file you needed.
- the stale-job watchdog, TALONâs alarm for processing jobs that silently stall, now runs on hardware i own. the system that assumed every external source would eventually fail is sitting on metal i can physically touch.
the honest ledger
staging is live on the box. prod cutover waits on a go-live list i just finished assembling, and that list includes real hardening work on the internal segmentation, written down and sequenced before production traffic, not after. a build log that only records the finished parts is a brochure, but a build log that itemizes the unfinished security work on a live system is a different kind of document, so the specifics stay in the private docs until they are past tense.
and the 3090 stays on the desk, right where the intro said it would: yardstick, not server. there is a version of this where a Tailscale tunnel lets the datacenter box borrow the workstationâs gpu for inference, and i think that version happens eventually. weâll see.
close
the cloud was never the wrong answer. it was the right answer to a question i eventually stopped asking. the question changed shape when the workload did, and the honest response was to match the infrastructure to the work instead of to the fashion. the box on my desk taught me what compute is supposed to feel like. now the box in the datacenter feels the same way, and the bill went up forty-nine dollars.