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.

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
CMDis not your command.
It’s the default argument toENTRYPOINT.
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 yourCMDYour 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 successGlobbing 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 handledIt’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.




