Dockerfile’s EXPOSE instruction is documentation (a port the app uses), -p is the runtime port mapping (host:container), the port mapping is what makes the container reachable from outside. The right answer is EXPOSE in the Dockerfile for documentation, -p (or --publish) at runtime for the actual mapping. The mistake every team makes: the team thinks EXPOSE makes the port accessible from the host, the team is confused when the port is not reachable — EXPOSE is documentation, the mapping is the runtime -p flag.
Table of contents
- The EXPOSE instruction — documentation, not networking
- The -p flag — the runtime port mapping
- The Dockerfile run command — the right pattern
- The port mapping — the network mode matters
- The Dockerfile port — the right pattern for a Node app
- The Dockerfile port — the right pattern for a Python app
- The platform port — the right answer for production
- How this fits the rest of the stack
- FAQ
The EXPOSE instruction — documentation, not networking
The EXPOSE instruction in the Dockerfile documents the port the app uses. EXPOSE 3000 says ‘this container listens on port 3000’. The instruction is metadata — it does not actually publish the port. The right answer is to use EXPOSE for every port the app uses, the right answer is to also document the protocol (EXPOSE 3000/tcp for clarity, the default is TCP).
The gotcha: EXPOSE does not make the port reachable from the host. The team that runs docker run myimage and expects port 3000 to be reachable on the host is confused — the port is not published, the host cannot reach the container. The right answer is to use -p 3000:3000 to publish the port.
The -p flag — the runtime port mapping
The -p flag (or --publish) maps a host port to a container port. -p 8080:3000 maps host port 8080 to container port 3000. The right answer is -p HOST:CONTAINER for the mapping, the right answer for a random host port is -P (uppercase) which maps the EXPOSEd ports to random host ports.
The gotcha: the team that uses -p 3000:3000 and the port is already in use on the host gets a cryptic error. The right answer is to check the host port first, the right answer for a production deploy is to use the platform’s port mapping (the platform’s manifest, the Kubernetes service).
The Dockerfile run command — the right pattern
The Dockerfile’s CMD (or ENTRYPOINT) is the command that runs when the container starts. The right answer is CMD ["node", "server.js"] (the array form, exec form) for a Node app, the right answer is CMD ["python", "app.py"] for a Python app. The wrong answer is the shell form (CMD node server.js) — the shell form runs the command in a shell, the team loses the signal handling (the container does not stop on SIGTERM).
The port mapping — the network mode matters
The default network mode is bridge. The container gets an IP on the bridge network, the port mapping is the only way to reach the container from the host. The right answer for a single-host setup is the default bridge, the right answer for a multi-container setup is a user-defined network (the containers can reach each other by name), the right answer for a single-container setup is --network host (the container uses the host’s network stack, no port mapping needed).
The gotcha: --network host is fast (no port mapping overhead) but the team loses the isolation (the container can see all the host’s network traffic). The right answer is --network host for a performance-critical single-container setup, the right answer is the default bridge for everything else.
The Dockerfile port — the right pattern for a Node app
The right pattern for a Node app’s Dockerfile:
FROM node:20-bookworm-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]
The right answer is to set PORT as an env var, the app reads process.env.PORT (or the platform sets it). The right answer is to EXPOSE 3000 for documentation, the right answer is to publish the port at runtime with -p 3000:3000 (or the platform’s mapping).
The Dockerfile port — the right pattern for a Python app
The right pattern for a Python app’s Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV PORT=8000
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
The right answer is --host 0.0.0.0 (the app binds to all interfaces, not just localhost). The wrong answer is --host 127.0.0.1 — the app is reachable from inside the container, not from outside. The right answer is to use the platform’s port, not hardcode 8000 in the CMD.
The platform port — the right answer for production
The right answer for a production deploy is to use the platform’s port mapping. Kubernetes: the Service maps the cluster IP to the pod, the Ingress maps the external traffic to the Service. Heroku: the PORT env var is set by the platform, the app reads it. Render: the PORT is 10000 by default, the app reads it. The wrong answer is to hardcode the port — the platform’s port is not the team’s port.
How this fits the rest of the stack
The infrastructure question is a small piece of a larger pattern: the team’s runtime, storage, database, secret store, logs, and deployment platform are all parts of the same platform. The right answer is to model the full stack before the project ships, not after. The RunxBuild hosting calculator is the right place to do that exercise — pick the runtime, the memory tier, the storage, the secret store, and the egress, and the calculator shows what the deploy actually costs at the team’s actual usage.
Useful related references:
FAQ
What is EXPOSE in a Dockerfile?
Documentation. It says ‘this container listens on this port’. It does not actually publish the port.
How do I make a port reachable from the host?
Use -p HOST:CONTAINER at runtime, or --network host for a single-container setup. EXPOSE alone does not make the port reachable.
What is the difference between EXPOSE and -p?
EXPOSE is documentation in the Dockerfile. -p is the runtime port mapping. The team needs both for the port to be reachable.
What port should I use in a Dockerfile?
The standard port for the framework (3000 for Node, 8000 for Python, 8080 for Java). The right answer is to make the port configurable via env var, the app reads it.
Should I use —network host?
For a performance-critical single-container setup, yes. For everything else, no — the team loses the isolation.
What is the difference between -p and -P?
-p maps a specific HOST:CONTAINER. -P maps the EXPOSEd ports to random host ports.
How do I check if a port is in use on the host?
lsof -i :<port> on macOS/Linux, netstat -ano | findstr :<port> on Windows. The right answer is to check before running docker run.
Why is my container not reachable from the host?
Because EXPOSE alone does not publish the port. Use -p HOST:CONTAINER at runtime to make it reachable.