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

  1. During docker build, Docker reads the Dockerfile from top to bottom. FROM chooses the parent image layers, COPY and RUN can add new filesystem layers, and EXPOSE, CMD, and ENTRYPOINT update the image config.
  2. 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.
  3. 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.
  4. If the image has ENTRYPOINT and CMD, Docker combines them. With exec form, ENTRYPOINT ["program"] and CMD ["arg"] become program arg. Extra command arguments after the image name replace CMD.
  5. If you publish a port with -p, Docker configures host-to-container forwarding. EXPOSE alone does not do this step.
  6. 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 CMD and ENTRYPOINT unless you specifically need shell features such as pipes or variable expansion.
  • Use ENTRYPOINT for command-like images where the executable should stay fixed, such as a backup tool, migration runner, or static file server.
  • Use CMD alone for normal application images where developers may reasonably replace the command for debugging.
  • Combine ENTRYPOINT and CMD when the program is fixed but the default arguments should be easy to override.
  • Treat EXPOSE as documentation and metadata. Always publish with -p or Compose ports: when the host needs access.
  • Pin base image tags such as python:3.12-alpine or node:20-alpine; avoid latest for 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, or ENV. Image layers and config can be inspected later.

Practice Exercises

  1. Create a Dockerfile for a small HTTP app that listens on container port 5000. Add the correct EXPOSE line, then write the docker run command that publishes it on host port 8080. Hint: the publishing flag needs both ports.
  2. 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.
  3. Inspect an image you built with docker image inspect --format and find its exposed ports, command, and entrypoint. Hint: look under the image’s .Config fields.

Summary

  • EXPOSE records intended container ports but never publishes them to the host by itself.
  • CMD supplies the default command or default arguments and is easy to override at docker run time.
  • ENTRYPOINT defines 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.