CMD vs ENTRYPOINT in Dockerfile
1. Introduction
[!NOTE] Question What is the difference between CMD and ENTRYPOINT in Dockerfile?
What we're trying to achieve: Understand when to use CMD vs ENTRYPOINT, how they work together, and how to override them when running containers.
Goal/Aim: By the end of this tutorial, you'll master the subtle but important differences between CMD and ENTRYPOINT, and know exactly which one to use in different scenarios.
2. How to Solve (Explained Simply)
Think of CMD and ENTRYPOINT like restaurant ordering:
ENTRYPOINT = The main course you must order
- "This is a pizza restaurant" - you're getting pizza
- Can't change the main item, only customize it
CMD = Default toppings/sides
- "By default, you get pepperoni pizza"
- You can easily change it to vegetarian or Hawaiian
Together:
- ENTRYPOINT: "You're ordering pizza"
- CMD: "Default is pepperoni"
- Customer can say: "I want Hawaiian instead" (overriding CMD)
- Customer can't say: "Give me sushi instead" (can't change ENTRYPOINT easily)
Key Differences:
- CMD: Provides defaults that can be easily overridden
- ENTRYPOINT: Defines the main executable that's harder to override
- Together: ENTRYPOINT + CMD = executable + default arguments
3. Visual Representation
┌────────────────────────────────────────────────────────────┐
│ CMD vs ENTRYPOINT │
└────────────────────────────────────────────────────────────┘
Using CMD only:
┌──────────────────────┐
│ Dockerfile │
│ FROM ubuntu │
│ CMD ["echo", "Hello"]│
└──────────────────────┘
↓
docker run myimage
→ Output: Hello
docker run myimage echo "Goodbye"
→ Output: Goodbye (CMD completely replaced)
Using ENTRYPOINT only:
┌──────────────────────────┐
│ Dockerfile │
│ FROM ubuntu │
│ ENTRYPOINT ["echo"] │
└──────────────────────────┘
↓
docker run myimage "Hello"
→ Output: Hello
docker run myimage "Goodbye"
→ Output: Goodbye (added as argument)
Using ENTRYPOINT + CMD:
┌──────────────────────────┐
│ Dockerfile │
│ FROM ubuntu │
│ ENTRYPOINT ["echo"] │
│ CMD ["Hello"] │
└──────────────────────────┘
↓
docker run myimage
→ Output: Hello (uses CMD default)
docker run myimage "Goodbye"
→ Output: Goodbye (CMD overridden)Override Comparison:
┌─────────────────────────────────────────────────────────────┐
│ Override Behavior │
├─────────────────────────────────────────────────────────────┤
│ │
│ CMD: │
│ docker run myimage [new_command] ← Easily overridden │
│ │
│ ENTRYPOINT: │
│ docker run --entrypoint [new] myimage ← Needs flag │
│ │
│ ENTRYPOINT + CMD: │
│ docker run myimage [args] ← Overrides CMD, keeps ENTRY │
│ │
└─────────────────────────────────────────────────────────────┘4. Requirements / What Needs to Be Gathered
Prerequisites:
- Basic Dockerfile knowledge
- Understanding of command-line arguments
- Familiarity with shell commands
- Docker installed
Conceptual Requirements:
- How Linux/Unix commands work
- What are command arguments?
- Understanding of executables vs arguments
- Shell form vs exec form syntax
Tools Needed:
- Docker
- Text editor
- Terminal
5. Key Topics to Consider & Plan of Action
Understanding Two Forms:
-
Shell Form
dockerfileCMD echo "Hello" ENTRYPOINT echo "Hello"- Runs in a shell ()text
/bin/sh -c - Variable substitution works
- Signals not properly handled
- Runs in a shell (
-
Exec Form (Recommended)
dockerfileCMD ["echo", "Hello"] ENTRYPOINT ["echo", "Hello"]- Runs directly, no shell
- Proper signal handling
- No variable substitution
- Preferred for production
When to Use What:
| Scenario | Use | Reason |
|---|---|---|
| Flexible command | CMD | Easy to override |
| Fixed executable | ENTRYPOINT | Ensures specific tool runs |
| CLI tool | ENTRYPOINT + CMD | Tool fixed, args flexible |
| Script with defaults | Both | Script is ENTRY, defaults are CMD |
| Simple container | CMD | Simplicity |
6. Code Implementation
Example 1: CMD Only
# Dockerfile
FROM ubuntu:22.04
# Install required package
RUN apt-get update && apt-get install -y curl
# Default command
CMD ["curl", "https://api.github.com"]# Build
docker build -t cmd-example .
# Run with default
docker run cmd-example
# Executes: curl https://api.github.com
# Override completely
docker run cmd-example echo "Hello World"
# Executes: echo "Hello World" (curl command replaced)
# Run different curl command
docker run cmd-example curl https://google.com
# Executes: curl https://google.comExample 2: ENTRYPOINT Only
# Dockerfile
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl
# Fixed executable
ENTRYPOINT ["curl"]# Build
docker build -t entry-example .
# Run - requires argument
docker run entry-example https://api.github.com
# Executes: curl https://api.github.com
# Add curl flags
docker run entry-example -I https://google.com
# Executes: curl -I https://google.com
# Can't easily run other commands
docker run entry-example echo "Hello"
# Tries to execute: curl echo "Hello" (fails)
# Override ENTRYPOINT (requires flag)
docker run --entrypoint bash entry-example
# Now runs bash insteadExample 3: ENTRYPOINT + CMD (Best Practice)
# Dockerfile
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl
# Fixed executable
ENTRYPOINT ["curl"]
# Default arguments
CMD ["https://api.github.com"]# Build
docker build -t entry-cmd-example .
# Run with defaults
docker run entry-cmd-example
# Executes: curl https://api.github.com
# Override CMD (add different URL)
docker run entry-cmd-example https://google.com
# Executes: curl https://google.com
# Override CMD (add flags and URL)
docker run entry-cmd-example -I https://google.com
# Executes: curl -I https://google.com
# ENTRYPOINT still enforcedExample 4: Practical Web Server
# Dockerfile
FROM nginx:alpine
# Copy custom config
COPY nginx.conf /etc/nginx/nginx.conf
# ENTRYPOINT ensures nginx always runs
ENTRYPOINT ["nginx"]
# Default arguments for nginx
CMD ["-g", "daemon off;"]# Run normally
docker run -p 8080:80 my-nginx
# Executes: nginx -g "daemon off;"
# Run with debug mode
docker run -p 8080:80 my-nginx -g "daemon off;" -e stderr
# Executes: nginx -g "daemon off;" -e stderrExample 5: Python Application
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
# Python interpreter is fixed
ENTRYPOINT ["python"]
# Default script to run
CMD ["app.py"]# Run default script
docker run my-python-app
# Executes: python app.py
# Run different script
docker run my-python-app test.py
# Executes: python test.py
# Run with flags
docker run my-python-app -u app.py
# Executes: python -u app.py (unbuffered output)Example 6: Node.js Application
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Use exec form with environment variable
ENTRYPOINT ["node"]
# Default entry point
CMD ["server.js"]# Run normally
docker run -p 3000:3000 my-node-app
# Executes: node server.js
# Run different file
docker run my-node-app worker.js
# Executes: node worker.js
# Run with node flags
docker run my-node-app --inspect=0.0.0.0 server.js
# Executes: node --inspect=0.0.0.0 server.jsExample 7: Using Shell Form (Be Careful!)
# Dockerfile - Shell Form
FROM ubuntu:22.04
# Shell form - runs in /bin/sh -c
CMD echo "The USER is $USER"# Variables work in shell form
docker run shell-example
# Output: The USER is root# Dockerfile - Exec Form with Variable
FROM ubuntu:22.04
ENV USER=appuser
# Exec form - no variable substitution
CMD ["echo", "The USER is $USER"]
# This will print literally: The USER is $USER
# Solution: Use shell explicitly
CMD ["sh", "-c", "echo The USER is $USER"]
# This will print: The USER is appuserExample 8: Wrapper Script Pattern
# Dockerfile
FROM postgres:15
# Copy custom entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Script handles initialization
ENTRYPOINT ["docker-entrypoint.sh"]
# Default command passed to script
CMD ["postgres"]# docker-entrypoint.sh
#!/bin/bash
set -e
# Custom initialization
echo "Running custom setup..."
# ... setup logic ...
# Execute the CMD
exec "$@"Example 9: CLI Tool Container
# Dockerfile for AWS CLI
FROM amazon/aws-cli:latest
# AWS CLI is the fixed command
ENTRYPOINT ["aws"]
# Default subcommand
CMD ["--help"]# Show help (default)
docker run my-aws-cli
# List S3 buckets
docker run my-aws-cli s3 ls
# Describe EC2 instances
docker run my-aws-cli ec2 describe-instances7. Things to Consider
Best Practices:
-
Use Exec Form
dockerfile# ✅ Good - exec form CMD ["nginx", "-g", "daemon off;"] # ❌ Avoid - shell form CMD nginx -g daemon off; -
ENTRYPOINT for Tools, CMD for Defaults
dockerfile# For CLI tools ENTRYPOINT ["aws"] CMD ["--help"] # For applications ENTRYPOINT ["python"] CMD ["app.py"] -
Make Scripts Executable
dockerfileCOPY entrypoint.sh / RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] -
Handle Signals Properly
dockerfile# Use exec to replace shell process CMD ["exec", "node", "server.js"] # Or in script: # exec "$@" # This is important!
Common Pitfalls:
❌ Using shell form (poor signal handling) ✅ Use exec form:
CMD ["executable", "arg1"]❌ Not using exec in scripts (PID 1 problem) ✅ Use
exec "$@"❌ Mixing forms incorrectly ✅ Be consistent with exec or shell form
❌ Hardcoding everything in ENTRYPOINT ✅ Use CMD for defaults that might change
Shell vs Exec Form Comparison:
| Aspect | 🐚 Shell Form | ⚡ Exec Form |
|---|---|---|
| Syntax |
text |
text |
| Process | /bin/sh -c "command arg" | Direct execution |
| PID 1 | Shell process | Your process |
| Signals | Not passed correctly | Handled properly |
| Variables | $VAR works | Need explicit shell |
| Performance | Slower (shell overhead) | Faster |
| Preferred | Rarely | Always for production |
| Aspect | Shell Form | Exec Form |
|---|---|---|
| Syntax | text | text |
| Process | text | Direct execution |
| PID 1 | Shell process | Your process |
| Signals | Not passed correctly | Handled properly |
| Variables | text | Need explicit shell |
| Performance | Slower (shell overhead) | Faster |
| Preferred | Rarely | Always for production |
8. Additional Helpful Sections
Decision Tree: CMD vs ENTRYPOINT
Do you want the command to be easily changeable?
│
├─ Yes → Use CMD only
│ Example: docker run myimage python script.py
│
└─ No → Is there a fixed executable?
│
├─ Yes → Use ENTRYPOINT (+ CMD for defaults)
│ Example: ENTRYPOINT ["python"]
│ CMD ["app.py"]
│
└─ No → Use CMD
Example: CMD ["python", "app.py"]Override Examples:
# Override CMD (easy)
docker run myimage new-command args
# Override ENTRYPOINT (requires flag)
docker run --entrypoint new-command myimage args
# Override both
docker run --entrypoint new-command myimage new-args
# In docker-compose
services:
app:
image: myimage
command: new-command args # Overrides CMD
entrypoint: new-entrypoint # Overrides ENTRYPOINTReal-World Patterns:
Pattern 1: Web Server
FROM nginx:alpine
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]Pattern 2: Database
FROM postgres:15
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"]Pattern 3: CLI Tool
FROM python:3.11
ENTRYPOINT ["python", "-m"]
CMD ["myapp.cli"]Pattern 4: Application
FROM node:18
ENTRYPOINT ["npm"]
CMD ["start"]Debugging Tips:
# See what command will run
docker inspect myimage --format='{{.Config.Cmd}}'
docker inspect myimage --format='{{.Config.Entrypoint}}'
# Run with shell for debugging
docker run -it --entrypoint /bin/sh myimage
# See what's actually executing
docker run myimage ps auxCommon Use Cases:
| Use Case | Pattern | Example |
|---|---|---|
| Web app | ENTRY + CMD | text text |
| Database | Script ENTRY + CMD | text text |
| CLI tool | ENTRY + help CMD | text text |
| Batch job | CMD only | text |
| Development | CMD only | text |
Summary
CMD and ENTRYPOINT serve different purposes in Docker: CMD provides default commands that are easily overridden, while ENTRYPOINT defines a fixed executable that makes the container behave like a command-line tool. Use CMD only when you want flexibility, ENTRYPOINT only for CLI tools, and both together when you have a fixed executable with configurable arguments (best practice). Always prefer exec form
["cmd", "arg"]ENTRYPOINT ["executable"]CMD ["default", "args"]