← Back to list

Your Docker Container Works Locally — But It Will Fail in Production

Ark Protocol · 2026-05-25 16:14 · 2 claps · 4.1 min read paywalled
#devops #docker #web-development #programming #technology
Open on Medium ↗
Wiki topics: 💻 · Programming 🌐 · Web Development ☁️ · DevOps & Cloud

Your Docker Container Works Locally — But It Will Fail in Production

The Slack message was three words. “Production is down.” No question mark. No context. Just a statement sitting in your notifications like a small explosion waiting for you to read it.

You had tested that container yourself. You had watched it start, watched it serve requests, watched the logs scroll cleanly across your terminal. You pushed the image with full confidence.

And somewhere between your laptop and the server, something invisible and silent just broke.

That invisible thing has a name. Actually, it has four names. And at least one of them has probably already burned you.

The Environment That Has Been Lying To You

Your laptop is a comfortable, familiar place. It has your .env file sitting quietly in the root directory, your local volumes, your exact runtime version, libraries you installed months ago and completely forgot about.

Production has none of that warmth. Production is a bare room with white walls that knows only what you explicitly handed it.

Every developer who has ever had a container collapse in production forgot — even briefly — that those two environments are fundamentally different machines. Your laptop is not a staging environment.

It is a deeply customized personal computer that happens to be running Docker. That distinction sounds small. It is not small at all.

The Secret Your .dockerignore Has Been Keeping

Here is the painful irony that gets developers every single time. You followed best practices. You added .env to your .dockerignore file so that secrets would never accidentally get baked into the image. That is the correct move. Responsible engineering.

But then you pushed to production and the app could not connect to the database, could not find the API key, could not do a single useful thing.

Because .env was never inside the image. And you never set those variables anywhere in the production environment either.

FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]

The image builds without a single error. It runs perfectly locally because some Docker setups pick up your local .env file automatically. Production picks up nothing automatically. Production waits.

Pass the configuration explicitly at runtime, every single time:

docker run --env-file .env.prod myapp:latest

The image carries your code. The environment carries your secrets. Those are two completely separate responsibilities that should never collapse into one.

Your Mac Has Been Building The Wrong Thing

M1 and M2 Macs build Docker images for arm64 architecture by default. Your production server is almost certainly amd64. These two architectures are not interchangeable, and Docker will not warn you loudly or clearly enough when you cross them.

You will see this error and spend real hours questioning your own code:

exec /usr/local/bin/node: exec format error

That is not a code problem. That is a wrong-architecture problem. Fix it at build time and never think about it again:

docker buildx build --platform linux/amd64 -t myapp:latest .

Better yet, make it structural inside your CI pipeline so the decision is never left to memory:

- name: Build Image
  run: |
    docker buildx build \
      --platform linux/amd64 \
      -t myapp:${{ github.sha }} \
      --push .

One flag. One line. An entire class of production failure permanently eliminated.

The localhost That Goes Absolutely Nowhere

Inside a Docker container, localhost means the container itself. Not your host machine. Not the database container sitting right next to it on the same server. Just the container you happen to be inside at that moment.

Your Mental Model (The Wrong One):
┌──────────────────────────────────────────┐
│  API container calls localhost:5432      │
│  Postgres feels like it is "right there" │
│  Everything feels connected              │
└──────────────────────────────────────────┘

What Docker Actually Sees:
┌──────────────────────────────────────────┐
│  API container  →  localhost:5432        │
│                         |               │
│                         X  (void)       │
│                                         │
│  Postgres lives in its own namespace    │
│  localhost inside A never reaches B     │
└──────────────────────────────────────────┘

Docker Compose creates an internal network where service names automatically become hostnames. Use them exactly as written:

services:
  api:
    build: .
    environment:
      - DB_HOST=db
  db:
    image: postgres:15
    environment:
      - POSTGRES_PASSWORD=secret

DB_HOST is db. Not localhost. Not 127.0.0.1. The service name. That single word change is the entire fix, and it is the kind of fix that makes you sit back and feel genuinely foolish for a moment before you move on.

The Silent One That Catches Senior Developers

This is the trap that nobody writes about, and the one that has quietly broken the confidence of developers who absolutely should have known better by now.

You change the base image in your Dockerfile. You run docker build. Docker shows activity in the terminal, layers being evaluated, everything looks fresh and correct. But Docker cached the previous layers and silently served your old code straight to production without a single warning.

You deploy. Nothing reflects the change. You deploy again. Still nothing. You start reading the same lines of code repeatedly, convinced you are missing something obvious.

docker build --no-cache -t myapp:latest .

That flag forces Docker to tear down every cached layer and rebuild completely from scratch. Use it whenever you change the base image, update system-level dependencies, or find yourself staring at a deployment that refuses to reflect your changes.

The build cache is a performance feature. It is not a contract. It is not a guarantee. And in the wrong moment, it becomes a very convincing liar.

What All Four Of These Are Actually Telling You

Every single one of these failures shares the same root cause. An assumption that felt so reasonable it never got verified.

You assumed the variables existed in production because they existed on your machine.

You assumed the architecture matched because you never thought to ask. You assumed localhost worked the way it works outside a container. You assumed the build cache was showing you something current.

Production does not operate on assumptions. It operates on exactly what you gave it and nothing beyond that.

The developers who stop repeatedly fighting these battles are not the ones who memorized every Docker flag.

They are the ones who learned — sometimes painfully — to treat every assumption as an open question that deserves a real answer before anything gets pushed.


메타데이터
post_id
9e0ecffa7c3f
slug
your-docker-container-works-locally-but-it-will-fail-in-production-9e0ecffa7c3f
url
https://medium.com/@ArkProtocol1/your-docker-container-works-locally-but-it-will-fail-in-production-9e0ecffa7c3f
canonical_url
https://medium.com/@ArkProtocol1/your-docker-container-works-locally-but-it-will-fail-in-production-9e0ecffa7c3f
author_url
https://medium.com/@ArkProtocol1
status
ok
fetched_at
2026-06-09 15:37:30