Skip to content
Go back

Docker CMD vs ENTRYPOINT: The Final Answer

By SumGuy 5 min read
Docker CMD vs ENTRYPOINT: The Final Answer

Here’s the thing: CMD and ENTRYPOINT are confusing because they try to solve different problems, but people use them interchangeably. Once you understand what each does, it clicks.

ENTRYPOINT is the command that always runs. CMD is the default arguments to that command. But it’s more nuanced than that, and the difference between shell and exec form changes everything.

Shell Form vs Exec Form

This matters more than you think.

Shell form:

ENTRYPOINT python app.py
CMD echo "hello"

Exec form:

ENTRYPOINT ["python", "app.py"]
CMD ["echo", "hello"]

The difference:

Why does this matter? Signals. If your container is PID 1 (the shell in shell form), it doesn’t receive SIGTERM and SIGINT properly. That’s why you should almost always use exec form.

The Behavior Matrix

This is where it gets tricky. Docker’s behavior depends on both ENTRYPOINT and CMD, and whether each uses shell or exec form.

Case 1: ENTRYPOINT (exec) + CMD (exec)

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

What runs: python app.py

CMD becomes arguments to ENTRYPOINT. Clean, predictable.

Invoke it:

Terminal window
$ docker run myapp
# Runs: python app.py
$ docker run myapp --debug
# Runs: python --debug
# (replaces CMD, but ENTRYPOINT stays)

Case 2: ENTRYPOINT (shell) + CMD (shell)

ENTRYPOINT python app.py
CMD python app.py

What runs: /bin/sh -c "python app.py"

The shell is PID 1. Don’t do this. Signals won’t work.

Case 3: ENTRYPOINT (exec) + CMD (shell)

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

What runs: /bin/sh -c "python app.py"

The shell from CMD wrapper everything. Broken.

Case 4: Only CMD (exec)

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

What runs: python app.py (directly as PID 1)

This works, but limits flexibility.

Case 5: Only ENTRYPOINT (exec)

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

What runs: python app.py (directly)

Also works, but you can’t override with arguments.

The Pattern: ENTRYPOINT + CMD Together

The recommended pattern:

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

This means:

You get flexibility: arguments override CMD, and —entrypoint overrides everything.

Real Examples

Web Server

Dockerfile
FROM node:18
WORKDIR /app
COPY . .
ENTRYPOINT ["node"]
CMD ["index.js"]
Terminal window
$ docker run myapp
# Runs: node index.js
$ docker run myapp --inspect
# Runs: node --inspect
# (useful for debugging)
$ docker run --entrypoint npm myapp test
# Runs: npm test
# (completely different command)

Python CLI Tool

Dockerfile
FROM python:3.11
WORKDIR /app
COPY . .
ENTRYPOINT ["python", "-u"] # -u = unbuffered output
CMD ["main.py"]
Terminal window
$ docker run mytool
# Runs: python -u main.py
$ docker run mytool --help
# Runs: python -u --help
$ docker run --entrypoint bash mytool
# Runs: bash
# (drop into shell for debugging)

Database Backup Script

Dockerfile
FROM postgres:15
ENTRYPOINT ["pg_dump"]
CMD ["--help"]
Terminal window
$ docker run pgdump
# Runs: pg_dump --help
$ docker run pgdump -h myhost -U postgres mydb > backup.sql
# Runs: pg_dump -h myhost -U postgres mydb

Shell Form: When It’s Okay

There are cases where shell form makes sense:

FROM ubuntu
RUN apt-get update && apt-get install -y curl
ENTRYPOINT curl https://api.example.com

If you need environment variable interpolation:

ENTRYPOINT echo "Deployed at ${BUILD_TIME}" # ← Shell expands variables

Use shell form only if you need shell features (variable expansion, pipes, redirection). Otherwise, exec form.

The Debug Trick

When your container doesn’t start, you can drop into a shell:

Terminal window
$ docker run --entrypoint /bin/sh myapp -c "set -x; python app.py"
# ← Run with debug output

Or:

Terminal window
$ docker run --entrypoint /bin/bash myapp
root@abc123:/app# python app.py
# ← Interactive shell

This is why you want a basic shell in your image (not always true for distroless).

Distroless Pattern

If you use Google’s distroless images, there’s no shell, and you can’t override:

FROM gcr.io/distroless/nodejs18-debian11
COPY app /app
WORKDIR /app
ENTRYPOINT ["/nodejs/bin/node", "index.js"]

Distroless images are secure and minimal. Trade-off: you can’t easily debug with --entrypoint /bin/bash. Plan accordingly.

Compose Override

In Docker Compose, you can override:

docker-compose.yml
services:
api:
build: .
entrypoint: ["python"]
command: ["app.py", "--debug"]

This is cleaner than command-line flags for persistent configs.

Checklist for Your Dockerfile

Quick Reference

# Best practice
ENTRYPOINT ["python"]
CMD ["app.py"]
# Usage
docker run myapp # python app.py
docker run myapp --debug # python --debug
docker run --entrypoint bash myapp # bash

CMD and ENTRYPOINT aren’t magic. They’re just a way to define what your container does. Exec form + the pattern above gives you the most flexibility with the least pain.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
Diagnosing Slow Linux Boot with systemd-analyze
Next Post
SSHFS: Ditch SCP & Access Remote Files

Related Posts