EXPOSE, CMD, and ENTRYPOINT
EXPOSE, CMD, and ENTRYPOINT describe how an image is meant to run. EXPOSE records the ports the application listens on, while CMD and ENTRYPOINT decide the default process started by docker run. These instructions matter because a Docker image is not only a filesystem snapshot; it also carries metadata that Docker uses when it creates a container.
Overview: How They Work
A Dockerfile builds an image from ordered instructions. Filesystem-changing instructions such as RUN, COPY, and ADD create image layers. Runtime instructions such as EXPOSE, CMD, and ENTRYPOINT mostly update the image configuration metadata. That metadata is stored with the image manifest and config object, then read later by the Docker daemon when a container is created.
EXPOSE is documentation for the container side of a network port. If an app inside the container listens on port 3000, EXPOSE 3000 tells humans and tools that port 3000 is expected. It does not publish the port to your laptop, server, or LAN. Publishing happens at run time with docker run -p 8080:3000, or in Compose with ports:. Without publishing, another container on the same Docker network may be able to reach the app, but your host browser usually cannot.
CMD provides the default command and arguments for the container. If you run the image without extra command arguments, Docker uses CMD. If you put a command after the image name in docker run, that command replaces CMD. Only the last CMD in a Dockerfile is used.
ENTRYPOINT defines the executable that should always run when the container starts. Command-line arguments after the image name are appended to the exec-form ENTRYPOINT instead of replacing it. A common pattern is ENTRYPOINT for the program and CMD for default arguments. For example, ENTRYPOINT ["python", "-m", "http.server"] plus CMD ["8000"] starts a web server on port 8000, while docker run image 9000 keeps the same program and changes only the argument.
Syntax
EXPOSE 3000
EXPOSE 8080/tcp
EXPOSE 8125/udp
CMD ["executable", "arg1", "arg2"]
CMD command arg1 arg2
ENTRYPOINT ["executable", "arg1"]
ENTRYPOINT command arg1
| Instruction | Preferred form | Meaning |
|---|---|---|
EXPOSE |
EXPOSE 3000 |
Adds port metadata to the image. TCP is the default protocol. It does not publish the port. |
CMD |
CMD ["node", "server.js"] |
Sets the default command or default arguments. It is easy to override with docker run image command. |
ENTRYPOINT |
ENTRYPOINT ["node", "server.js"] |
Sets the container’s main executable. Extra run arguments are appended when exec form is used. |
The JSON-array style is called exec form. Prefer it for CMD and ENTRYPOINT because Docker starts the process directly without inserting a shell. Shell form runs through /bin/sh -c on Linux containers, which can change signal handling, quoting, and environment-variable expansion. For long-running services, exec form usually stops more cleanly because the application receives signals such as SIGTERM directly.
Examples
Example 1: A Node App With a Documented Port
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]
Output:
Successfully built 7f3a2c8b9d10
Successfully tagged node-web:1.0
This Dockerfile uses the pinned base image node:20-alpine instead of node or latest, which makes builds more reproducible. EXPOSE 3000 records that the application expects to listen on container port 3000. CMD says that the default container process is node server.js. The dependency files are copied before the app source so Docker can reuse the npm ci layer when only server.js changes.
docker build -t node-web:1.0 .
docker run --rm -p 8080:3000 node-web:1.0
Output:
Server listening on port 3000
The important part is -p 8080:3000. The left side is the host port, and the right side is the container port. Browsing to http://localhost:8080 reaches port 3000 inside the container. If you omit -p, EXPOSE still exists as image metadata, but nothing is published to the host.
Example 2: ENTRYPOINT With Overridable Default Arguments
FROM python:3.12-alpine
WORKDIR /site
COPY index.html ./
EXPOSE 8000
ENTRYPOINT ["python", "-m", "http.server"]
CMD ["8000"]
Output:
Successfully built 2a42d0c9f511
Successfully tagged static-site:1.0
This image always starts Python’s static file server because that program is the ENTRYPOINT. The default argument is 8000, supplied by CMD. If the user runs the image without arguments, the final process is equivalent to python -m http.server 8000.
docker run --rm -p 8080:8000 static-site:1.0
docker run --rm -p 9090:9000 static-site:1.0 9000
Output:
Serving HTTP on 0.0.0.0 port 8000
Serving HTTP on 0.0.0.0 port 9000
The second command appends 9000 to the entrypoint, replacing the default CMD argument. Notice that the published port also changed to -p 9090:9000. If the process listens on 9000 but you publish 8080:8000, traffic goes to the wrong container port.
Example 3: Inspecting Image Metadata
docker image inspect static-site:1.0 --format '{{json .Config.ExposedPorts}}'
docker image inspect static-site:1.0 --format '{{json .Config.Entrypoint}}'
docker image inspect static-site:1.0 --format '{{json .Config.Cmd}}'
Output:
{"8000/tcp":{}}
["python","-m","http.server"]
["8000"]
docker image inspect shows that these instructions are stored as image configuration. They are not hidden magic in the container filesystem. When you run the image, Docker combines the stored configuration with your run-time flags and any command arguments you provide.
How It Works Step by Step
- During
docker build, Docker reads the Dockerfile from top to bottom.FROMchooses the parent image layers,COPYandRUNcan add new filesystem layers, andEXPOSE,CMD, andENTRYPOINTupdate the image config. - At the end of the build, Docker stores a content-addressed image: layers plus a config object. The config includes exposed ports, environment variables, working directory, entrypoint, and command.
- When you run
docker run, the Docker client sends a create-container request to the daemon. The daemon reads the image config, creates a thin writable container layer, configures networking, and prepares the process specification. - If the image has
ENTRYPOINTandCMD, Docker combines them. With exec form,ENTRYPOINT ["program"]andCMD ["arg"]becomeprogram arg. Extra command arguments after the image name replaceCMD. - If you publish a port with
-p, Docker configures host-to-container forwarding.EXPOSEalone does not do this step. - The container stays alive only while its main process is running. When the process exits, the container stops, even if the image still exists.
Common Mistakes
Thinking EXPOSE Publishes a Port
FROM nginx:1.27-alpine
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
This image documents port 80, but running it as docker run nginx-demo:1.0 does not make it available on localhost:80. The fix is to publish the port at run time:
docker run --rm -p 8080:80 nginx-demo:1.0
Now host port 8080 forwards to container port 80.
Using Shell Form for a Long-Running Service
CMD node server.js
Shell form is valid, but for services it can cause awkward signal behavior because the shell may become process ID 1 and sit between Docker and the real app. Prefer exec form:
CMD ["node", "server.js"]
With exec form, Docker starts node directly, so stop signals are delivered to the application process more predictably.
Overriding CMD When You Meant to Pass an Argument
docker run --rm node-web:1.0 --help
If the image uses only CMD ["node", "server.js"], the --help token replaces the whole command and Docker tries to run --help as the executable. When you need a fixed executable with adjustable arguments, use ENTRYPOINT for the executable and CMD for defaults.
Best Practices
- Use exec form for
CMDandENTRYPOINTunless you specifically need shell features such as pipes or variable expansion. - Use
ENTRYPOINTfor command-like images where the executable should stay fixed, such as a backup tool, migration runner, or static file server. - Use
CMDalone for normal application images where developers may reasonably replace the command for debugging. - Combine
ENTRYPOINTandCMDwhen the program is fixed but the default arguments should be easy to override. - Treat
EXPOSEas documentation and metadata. Always publish with-por Composeports:when the host needs access. - Pin base image tags such as
python:3.12-alpineornode:20-alpine; avoidlatestfor reproducible builds. - Keep dependency installation before source-code copy when possible, so ordinary code edits do not invalidate expensive dependency layers.
- Do not bake secrets into
CMD,ENTRYPOINT, orENV. Image layers and config can be inspected later.
Practice Exercises
- Create a Dockerfile for a small HTTP app that listens on container port
5000. Add the correctEXPOSEline, then write thedocker runcommand that publishes it on host port8080. Hint: the publishing flag needs both ports. - Build a utility image where the executable is fixed as
python, but the default argument is--version. Then run the same image with a different argument. Expected end state: the command after the image name replaces the default argument, not the executable. - Inspect an image you built with
docker image inspect --formatand find its exposed ports, command, and entrypoint. Hint: look under the image’s.Configfields.
Summary
EXPOSErecords intended container ports but never publishes them to the host by itself.CMDsupplies the default command or default arguments and is easy to override atdocker runtime.ENTRYPOINTdefines the main executable and is useful when the image behaves like a command.- Exec form avoids an extra shell and usually gives better signal handling for services.
- Docker stores these instructions as image configuration, then combines them with run-time options when creating a container.
