Using Docker for Application Deployment: The Boring Win I Underrated
TIL that 'works on my machine' stopped being a real category of bug the day I containerized the API, not because Docker is clever but because it removed an entire class of environment-drift I'd been debugging manually.
The Problem #
Deploying the Node.js API meant manually replicating my local environment on a server: the right Node version, the right global tooling, matching environment assumptions — and small mismatches (a different Node minor version, a missing system dependency) produced bugs that didn't reproduce locally and took real time to trace back to the environment rather than the code.
Context #
This started on a self-hosted VM managed by hand, where "deploy" meant SSHing in and running install/build commands directly against whatever the server's environment happened to be.
What I Tried #
Wrote a setup script that installed the exact Node version and dependencies the server needed, run manually on each deploy.
What Went Wrong #
The script drifted from reality over time — a system package got updated on the server outside the script, or a new dependency was added locally and the script wasn't updated to match — and each drift reintroduced exactly the "works locally, fails on the server" bug class I was trying to eliminate.
The Solution #
Containerized the API with Docker, so the exact runtime — Node version, system dependencies, everything — is defined once, in the Dockerfile, and built into an image that runs identically regardless of what the host machine happens to have installed.
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
CMD ["node", "server.js"]Why It Works #
A container image bundles the entire runtime environment as a build artifact, not a set of instructions that has to be re-run correctly against a host that might have drifted. "Works on my machine" stops being possible to say, because the container running in production is the same image I built and tested locally — not a re-creation of it.
Lessons Learned #
I'd filed Docker under "useful for complex microservices setups" and underrated how much it solved for a single, simple API — the value wasn't orchestration, it was eliminating environment drift as a bug category entirely.
What I Would Do Differently #
I'd containerize from the first deploy instead of maintaining a manual setup script and only reaching for Docker once environment drift had already caused a few hard-to-diagnose production bugs.
Related Concepts #
Reproducible build artifacts, environment drift, multi-stage Docker builds for smaller production images.
Related content
Choosing Node.js Hosting for a SaaS: What Actually Mattered vs. What I Thought Would
TIL that I picked Render for a Node.js API expecting the deciding factor to be price, and the thing that actually mattered day to day turned out to be deploy simplicity and predictable cold-start behavior.
Using Traefik as a Reverse Proxy for a Self-Hosted Stack
TIL that manually editing an Nginx config and restarting it for every new service didn't scale past a handful of containers — Traefik's label-based routing removed that step entirely by discovering services automatically.
Serving Premium Downloads Without Exposing the File's Real Storage URL
TIL how to replace a guessable direct-download link with a permission-checked, time-limited presigned URL, so the object storage path a purchased asset actually lives at is never sent to the client.