How Backend Devs Stop 'Works on My Machine' Bugs

For Backend developers hitting 'works on my machine' failures · Based on Piyush Docker-to-Kubernetes Container Fundamentals Framework

// TL;DR

If your API works locally and in Test but crashes in Prod with missing-library errors, the Build-Ship-Run framework fixes it. You author one Dockerfile that packages your code, dependencies, and a minimal OS into a single image, push it to a registry, then pull and run that exact same image everywhere. Because dependencies travel inside the image, environment misconfiguration failures cannot recur. Use this whenever you're moving from environment-based deployments to containers, or when you keep firefighting Prod-only bugs that never appear locally.

Why does my app work locally but crash in production?

Because your Prod environment is missing a library, dependency, or configuration that exists on your laptop and your Test server. Piyush calls this environment misconfiguration, and it's the single most frequent cause of build-promotion failures in pre-container workflows. A Node.js API that runs fine on your machine and passes in Test can crash in Prod with a missing-library error simply because the exact package version was never installed there.

The Build-Ship-Run framework eliminates this class of failure by making the environment irrelevant. You package the application code, all libraries, dependencies, and a bare-minimum OS image together as one shippable unit — the Docker image. That unit runs identically everywhere.

How do I containerise my backend service?

Follow the workflow in order. First, diagnose the promotion problem: document exactly what was present in Dev and Test but missing in Prod. If it's a dependency or config difference, containers solve it directly.

Then author the Dockerfile — one file named exactly `Dockerfile` with a capital D:

1. Select a base image (e.g. a Node base image).

2. Install the exact dependencies (`npm install`).

3. Copy your application source into the image.

4. Define the start command.

Keep it lean — only the libraries your app actually needs. This is the One Dockerfile Per Application principle: a single source of truth for how the image is built.

Next, build with `docker build`. The Docker Client sends this to the Docker Daemon (dockerd), which reads your Dockerfile and produces a versioned image stored locally.

How do I get the same image running in every environment?

Never deploy straight from your build host. Ship the image with `docker push` to a registry — Docker Hub, Artifact Registry, JFrog Artifactory, or Nexus. Think of the registry as GitHub for your binaries: versioned, trackable, auditable storage that makes promotion safe.

Then on each environment, run the image: `docker pull` retrieves the identical image, and `docker run` spins up the container. Because Dev, Test, and Prod all pull the same image, the missing-library failure that plagued your API cannot happen again — the library is baked inside.

What mistakes trip up backend developers first?

- Pushing directly to Prod without a registry. This removes versioning and breaks safe promotion. Always ship through the registry.

- Rebuilding per environment. If you rebuild in each environment you reintroduce drift. Build once, push once, pull everywhere.

- Using floating version tags in your Dockerfile. Pin dependency versions so the same image is genuinely reproducible.

- Confusing image and container. The image is the shippable package; only `docker run` turns it into a live container instance.

- Naming the file `dockerfile` or `Dockerfile.txt`. Use the exact default name so `docker build` detects it.

How do I know the pipeline is correct?

Validate against the Docker component model: Docker Client → Docker Daemon → local image storage → registry → Docker Daemon on target → Container Runtime → running container. Each of build, push, pull, and run maps to a specific component, so any breakage is traceable to one place. If a container fails to start, you know to look at the run command and the Container Runtime, not the registry.

Next step: Take your most fragile service — the one with recurring Prod-only bugs — and write its first Dockerfile today. Build it, push it to Docker Hub, then pull and run it in a clean environment. When it runs identically, you've eliminated an entire class of deployment failure.

// FREQUENTLY ASKED QUESTIONS

Which base image should I use for a Node.js API?

Start with an official Node base image that matches your runtime version, then keep it lean by installing only the npm dependencies your app needs. Following the Lightweight Sandbox principle, avoid bundling a full OS or unused packages — smaller images pull faster and run more densely. Pin the Node version so the same image is reproducible across every environment.

Do I need a registry if I'm the only developer?

Yes. The registry is version control for your binaries, giving you versioned, trackable image storage and a safe promotion path from Dev to Prod. Even solo, pushing to Docker Hub or another registry means every environment pulls the identical image rather than a locally rebuilt variant that could drift. Skipping it reintroduces the exact failures containers exist to prevent.

How do I stop a dependency version from silently changing?

Pin exact versions in your Dockerfile's install step rather than using floating tags. A floating version can resolve to a different library at build time, reintroducing environment misconfiguration. Pinning ensures the built image contains the precise dependencies your app was tested with, so the same image genuinely behaves identically across Dev, Test, and Prod.