Article

CMD vs ENTRYPOINT in Docker: The Ultimate Guide to Container Instructions

6 min read
Originally published on Medium. Read, clap or comment on Medium ↗

Understanding the subtle but crucial differences that can make or break your containerization strategy


When you’re crafting Dockerfiles, two instructions often cause confusion among developers: CMD and ENTRYPOINT. At first glance, they seem to do the same thing—define what runs when your container starts. But beneath the surface lies a world of difference that can dramatically impact how your containers behave in production.

If you’ve ever wondered why your container arguments aren’t working as expected, or why some containers seem “locked” while others are flexible, you’re about to discover the answer.

The Great Container Startup Mystery

Picture this: You’re deploying a web application, and you want your container to start your app server. Simple enough, right? You could use either:

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

or

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

Both will start your application, but here’s where things get interesting. What happens when someone runs your container like this?

docker run myapp ls -la

With CMD, they'll get a directory listing instead of your application. With ENTRYPOINT, they'll get an error. This fundamental difference shapes everything about how your containers behave in the wild.

CMD: The Flexible Default

Think of CMD as setting a default behavior for your container—one that can be completely overridden. It's like setting a default ringtone on your phone; it works great until someone decides they want something different.

The Three Faces of CMD

CMD comes in three flavors, each with its own personality:

Exec Form (Recommended)

CMD ["executable", "param1", "param2"]

This is the gold standard. It runs your command directly without invoking a shell, making it faster and more predictable. No shell means no variable expansion, no wildcard matching, and no signal forwarding issues.

Shell Form

CMD command param1 param2

This wraps your command in /bin/sh -c, giving you shell features but at a cost. Your process won't be PID 1, which can cause problems with signal handling and zombie processes.

As Parameters to ENTRYPOINT

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

This creates a dynamic duo where ENTRYPOINT sets the base command and CMD provides default arguments.

When CMD Shines

CMD excels in scenarios where flexibility is king:

Development Containers

FROM node:16
WORKDIR /app
COPY . .
CMD ["npm", "start"]

Developers can easily override this to run tests, install dependencies, or debug:

docker run myapp npm test
docker run myapp npm install new-package
docker run myapp bash

Multi-Purpose Images

FROM ubuntu:20.04
RUN apt-get update && apt-get install -y curl wget
CMD ["bash"]

This creates a utility container that defaults to an interactive shell but can run any command.

ENTRYPOINT: The Immutable Foundation

ENTRYPOINT is like a bouncer at an exclusive club — it decides what gets in and what doesn’t. Unlike CMD, it cannot be overridden by command-line arguments. Instead, those arguments become additional parameters.

The ENTRYPOINT Advantage

Predictable Behavior

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

No matter how someone tries to run your container, they’ll always get your Python application. Any additional arguments become parameters to your script:

docker run myapp --debug --port 8080
# Executes: python app.py --debug --port 8080

Application-Specific Containers

FROM postgres:13
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"]

PostgreSQL’s official image uses this pattern. The entrypoint script handles initialization, while CMD provides the default command. Users can override the command but not the essential setup logic.

The Power Couple: ENTRYPOINT + CMD

The real magic happens when you combine both instructions. This pattern creates containers that are both rigid where they need to be and flexible where it makes sense.

Real-World Example: A Python Web API

FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .# Create an entrypoint script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.shENTRYPOINT ["/entrypoint.sh"]
CMD ["python", "app.py"]

entrypoint.sh:

#!/bin/bash
set -e
# Always run database migrations
python manage.py migrate# Execute the main command
exec "$@"

This setup ensures migrations always run while allowing flexibility in the main command:

# Default behavior - runs migrations then starts the app
docker run myapi
# Override to run management commands
docker run myapi python manage.py createsuperuser# Override to get a shell
docker run myapi bash

Common Pitfalls and How to Avoid Them

The Shell Form Trap

Don’t do this:

CMD python app.py

Do this instead:

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

Shell form prevents your application from receiving signals properly, making graceful shutdowns nearly impossible.

The Signal Handling Problem

When you use shell form, your process isn’t PID 1 in the container. This means SIGTERM signals (used by docker stop) go to the shell, not your application:

# With shell form
docker stop mycontainer # Takes 10 seconds (SIGKILL timeout)
# With exec form
docker stop mycontainer # Stops immediately (app handles SIGTERM)

The Variable Expansion Confusion

Shell form expands variables:

ENV PORT=8080
CMD python app.py --port $PORT # Works: expands to --port 8080

Exec form doesn’t:

ENV PORT=8080
CMD ["python", "app.py", "--port", "$PORT"] # Doesn't work: literal "$PORT"

If you need variable expansion with exec form, use a wrapper script.

Decision Tree: CMD or ENTRYPOINT?

Use CMD when:

Use ENTRYPOINT when:

Use both when:

Advanced Patterns and Best Practices

The Init Script Pattern

FROM node:16-alpine
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-initWORKDIR /app
COPY package*.json ./
RUN npm ci --only=productionCOPY . .# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]

This ensures proper signal handling even in complex scenarios.

The Configuration Pattern

FROM alpine:latest
COPY configure.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/configure.shENTRYPOINT ["configure.sh"]
CMD ["default-app"]

configure.sh:

#!/bin/sh
# Setup based on environment variables
if [ "$ENVIRONMENT" = "production" ]; then
echo "Configuring for production..."
# Production-specific setup
else
echo "Configuring for development..."
# Development-specific setup
fi
exec "$@"

Performance and Security Implications

Performance Considerations

Exec form is faster:

Shell form adds overhead:

Security Implications

Exec form is more secure:

# Secure: No shell interpretation
ENTRYPOINT ["python", "app.py"]

Shell form can be vulnerable:

# Potentially vulnerable to shell injection
CMD python app.py $USER_INPUT

Testing Your Dockerfile Instructions

Always test both the default behavior and overrides:

# Test default behavior
docker run myapp
# Test with arguments
docker run myapp --help# Test complete override (if using CMD)
docker run myapp bash# Test signal handling
docker run -d --name test myapp
docker stop test # Should stop quickly

The Bottom Line

The choice between CMD and ENTRYPOINT isn’t just about syntax — it’s about defining the contract between your container and its users. CMD says “here’s what I usually do, but you’re free to change it.” ENTRYPOINT says “this is what I am, work with it.”

Understanding this distinction will help you build containers that behave predictably in production while remaining flexible enough for development and debugging. The key is matching the instruction to your use case and being intentional about the level of control you want to maintain.

Remember: good containers are like good APIs — they’re predictable, well-documented, and handle edge cases gracefully. Whether you choose CMD, ENTRYPOINT, or both, make sure your choice aligns with how your container will be used in the real world.


Want to dive deeper into Docker best practices? Follow me for more containerization insights and real-world DevOps strategies.

← All articles Discuss on Medium ↗