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:
- Building development or debugging containers
- Creating base images others will extend
- The primary use case involves running different commands
- You want maximum flexibility
Use ENTRYPOINT when:
- Building production application containers
- You need guaranteed initialization logic
- The container has a single, specific purpose
- You want to prevent accidental command override
Use both when:
- You need initialization logic AND flexibility
- Building production containers with debugging capabilities
- Following the “container as executable” pattern
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:
- No shell invocation overhead
- Direct process execution
- Better memory usage
Shell form adds overhead:
- Extra shell process
- Environment variable processing
- Potential for shell injection if not careful
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.