Skip to main content

Command Palette

Search for a command to run...

Your Ops Team Hates You: Because You Don't Know CMD vs ENTRYPOINT

How a small mistake in your Dockerfile can break shutdowns, spawn zombies, and make your containers useless.

Updated
5 min read
Your Ops Team Hates You: Because You Don't Know CMD vs ENTRYPOINT
K

Senior Platform Engineer. Infra and programming languages nerd. I write about the stuff nobody teaches: how things really work under the hood, containers, orchestration, authentication, scaling, debugging, and what actually matters when you’re building and running real systems. I share what I wish more real seniors did: the brutal, unfiltered truth about building secure and reliable systems in production.

Most Dockerfiles are broken, not because Docker is hard, but because developers misunderstand the difference between CMD and ENTRYPOINT.

This isn’t just about clean syntax. It’s about what actually runs in production.

What CMD Actually Does

CMD is not your command.
It’s the default argument to ENTRYPOINT.

If you don’t define an ENTRYPOINT, Docker will use /bin/sh -c behind the scenes (in some cases), or directly exec your CMD, depending on how you wrote it.

Here’s what that means:

JSON form (a.k.a. exec form)

CMD ["python", "app.py"]

Docker translates this to:

exec python app.py

No shell. No expansion. No surprises.
This is the form you should be using 99% of the time.

Shell form

CMD python app.py

Docker wraps this as:

sh -c "python app.py"

This does involve a shell. And shells do things you may not expect, like gobbling up signals, interpreting characters, or spawning subshell.

What Happens If You Use Only CMD

Let’s say your Dockerfile ends with:

CMD ["python", "server.py"]

Now someone runs your container like this:

docker run my-image echo "hello"

What happens?

  • The user-supplied command (echo "hello") overrides your CMD

  • Your app doesn’t run

  • The container echoes “hello” and exits

You just shipped a useless container.
Your server’s not running. Your health checks will fail.
And your Ops team will hate you.

Use ENTRYPOINT to Stay in Control

Here’s the right way to build the same container:

ENTRYPOINT ["python", "server.py"]

Now:

docker run my-image echo "hello"

Runs:

python server.py echo "hello"

Boom. Your app starts. The user’s input is passed as arguments.
You’re in control.

Want to give your app default arguments? Use both:

ENTRYPOINT ["python", "server.py"]
CMD ["--host=0.0.0.0"]

That runs:

python server.py --host=0.0.0.0

And still lets users do:

docker run my-image --port=3000

python server.py --port=3000

This is how production containers should behave:
Predictable. Overridable. Explicit.

Pro Tip: Don’t Use Shell Form Unless You Have To

Shell form (CMD somecommand) is useful if you need:

  • Shell operators like &&, |, >, && echo success

  • Globbing or expansion

But if you're just trying to run a binary or script?

Use JSON form (CMD ["mycommand", "arg"]).
It avoids ambiguity and doesn’t wrap your process in sh -c.

Why This Matters in Production?

If you're not careful, you’ll:

  • Lose control over what actually runs in the container

  • Break shutdown logic (when signals don’t reach your app)

  • Make it impossible to pass runtime args to your app

  • Confuse your team during debugging

But does this make it production-ready?

No. Not yet.

You also need to understand what PID 1 is and why it matters.

In Linux, the first process inside a container (PID1) has special behavior:

  • It doesn’t forward signals (SIGTERM, SIGINT) unless explicitly handled

  • It’s expected to reap orphaned zombie processes

  • If your app is PID 1 and doesn’t do this properly, it will behave incorrectly, especially during shutdown

Option 1: Use tini (the cleanest fix)

tini is a minimal init system that becomes PID 1 and does two things really well:

  • Forwards signals to your app (so it can shut down cleanly)

  • Reaps zombies if your app spawns any child processes

Here’s how you’d use it:

FROM node:20-slim

RUN apt-get update && apt-get install -y tini

ENTRYPOINT ["/usr/bin/tini", "--", "node", "server.js"]
CMD []

Option 2: Use an entrypoint wrapper script

If you don’t want to install extra binaries, you can mimic similar behavior with a shell script:

COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD []

docker-entrypoint.sh:

#!/bin/sh

trap 'kill -TERM "$child" 2>/dev/null' TERM INT

node server.js "$@" &
child=$!

wait "$child"

This handles signals manually and reaps the main child process. It’s not as clean or bulletproof as tini, but it's way better than nothing.

What About Compiled Languages like Go and Rust?

Compiled languages like Go and Rust don’t require an interpreter, so many devs assume they’re immune.

But guess what?

If your Go or Rust binary is PID 1, it still needs to:

  • Handle incoming signals (like SIGTERM, SIGINT)

  • Reap zombie child processes (if it spawns any)

If your compiled binary doesn't do these things, it still breaks in production.

Option A: Still use tini

ENTRYPOINT ["/tini", "--", "/app/myservice"]
CMD []

No need to reinvent signal handling or init logic.

Option B: Handle signals and zombies in code

In Go:

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

go func() {
    <-sigCh
    fmt.Println("Shutting down cleanly")
    os.Exit(0)
}()

This handles graceful shutdown, but it’s now your job to manage everything tini would have done for free (~23.5 KB)

Final Thoughts

Your Dockerfile isn’t just a build config. It defines how your app behaves in production. Misusing CMD and ENTRYPOINT might work on your machine, but it breaks silently when things get real: containers don’t start, shutdowns hang, signals vanish, and zombies pile up.

This stuff isn’t optional. If you’re building production containers, you need to understand how the init process works, what PID 1 actually does, and why relying on defaults is dangerous.

Use ENTRYPOINT to stay in control. Use tini or a proper entrypoint script to stay sane. And stop shipping containers that suck

Production is not the place to realize you misunderstood how Docker works.


I help engineering teams build smarter systems, from resilient containerization to scalable backend architecture.

If you care about shipping clean, production-ready software (and avoiding silent failures at scale), you’ll want to stick around.

Follow me on LinkedIn for brutally practical insights
Subscribe to this blog for deep dives, not developer fluff
Reach out if your stack needs clarity, performance, or a no-bullshit architecture review

I don’t write theory. I ship battle-tested systems.