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.

2 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

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.

Reproducible build artifacts, environment drift, multi-stage Docker builds for smaller production images.