Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Expose Ports with EXPOSE and -p

Learn how to expose container ports using EXPOSE in Dockerfiles and the -p flag at runtime, with hands-on steps and troubleshooting.

Focus: expose ports with expose and -p

Sponsored

Picture this: you build a perfect container image, run it with docker run my-app, and... nothing. No error, no crash — just silence. Your API server is running inside the container, but you can't reach it from your browser, Curl, or Postman. This is the single most common frustration for Docker newcomers — and it all comes down to understanding how ports travel between your host machine and the container's isolated network.

The problem this lesson solves

Containers are isolated by design — they have their own network stack, including IP addresses and ports. By default, nothing outside the container can connect to services running inside it. This means your web server listening on port 8080 inside the container is completely invisible to the outside world, including your own laptop. The pain is real: you think your app is running, but it's trapped in a network bubble. The solution lies in two Docker mechanisms: the EXPOSE instruction in your Dockerfile and the -p (or --publish) flag when you run a container.

Core concept / mental model

Think of EXPOSE as a label on a shipping box — it tells anyone who reads the label what's inside, but it doesn't actually open the box. EXPOSE is documentation for your image: "This container expects to offer service on port 8080." It does not publish ports; it only declares intent.

The -p flag, on the other hand, is cutting a door between your host machine and the container. When you run docker run -p 8080:80 my-web-app, you're creating a direct tunnel: traffic hitting your host on port 8080 gets forwarded to port 80 inside the container. Without this tunnel, the container's port 80 is unreachable even if you've EXPOSEd it.

Pro tip: EXPOSE is optional but recommended — it makes your intention clear and enables tools like Docker Compose and IDEs to auto-detect ports. Publishing with -p is mandatory for external access.

How it works step by step

Let's trace the full path of a web request from your browser to a containerized app:

  1. You define the service's port in your app code (e.g., an Express server listening on 0.0.0.0:3000).
  2. In the Dockerfile, you add EXPOSE 3000 — this serves as metadata in the image's configuration.
  3. You build the image: docker build -t my-web-app .
  4. At runtime, you use the -p flag: docker run -p 8080:3000 my-web-app
  5. Now your host's port 8080 is mapped to the container's port 3000.
  6. Open http://localhost:8080 in your browser — the request flows through the mapping to reach your app.

The mapping format

The complete syntax for -p is:

-p [host_ip:]host_port:container_port[/protocol]
  • host_port: the port on your host machine (e.g., 8080)
  • container_port: the port inside the container (e.g., 3000)
  • host_ip (optional): bind to a specific interface (e.g., 127.0.0.1 for localhost-only)
  • /protocol (optional, default is TCP): tcp or udp

Multiple ports

You can expose and publish multiple ports:

docker run -p 80:80 -p 443:443 -p 8080:8080 nginx

Random host port assignment

If you only care about the container port and want Docker to pick a random host port, omit the host port:

docker run -p 3000 my-web-app

Check which host port was assigned with docker ps or docker port <container-id>.

Hands-on walkthrough

Let's build a minimal Node.js app that listens on port 3000 and demonstrate the full flow.

Step 1: Create a simple app

mkdir docker-port-demo && cd docker-port-demo
cat > app.js << 'EOF'
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello from container on port 3000!\n');
});
server.listen(3000, '0.0.0.0', () => {
  console.log('Server running on port 3000');
});
EOF

Step 2: Write the Dockerfile

FROM node:18-alpine
WORKDIR /app
COPY app.js .
EXPOSE 3000
CMD ["node", "app.js"]

Step 3: Build and run without publishing

docker build -t port-demo .
docker run -d --name no-publish-test port-demo

Check the port status:

docker port no-publish-test
# Output: (nothing — no ports published)
curl http://localhost:3000
# Output: curl: (7) Failed to connect to localhost port 3000: Connection refused

Step 4: Run with -p publishing

# Stop previous container
docker stop no-publish-test && docker rm no-publish-test

# Run with explicit mapping
docker run -d --name published-test -p 8080:3000 port-demo

# Verify the mapping
docker port published-test
# Output: 3000/tcp -> 0.0.0.0:8080

# Test it
curl http://localhost:8080
# Output: Hello from container on port 3000!

Compare options / when to choose what

Scenario Use EXPOSE Use -p Why
Documenting container ports ✅ Always ❌ Not relevant Makes intent clear to other developers and tools
External access (from host) ❌ Not sufficient ✅ Required Without -p, nobody outside the container can connect
Inter-container communication (same Docker network) ❌ Not needed ❌ Not needed Containers communicate via their internal ports directly
Production deployment ✅ Recommended ✅ Required Both: document with EXPOSE, publish with -p (or compose file)
Local development ✅ Optional ✅ Required Usually need to access your app from browser

Variations on publishing

  • Port range: docker run -p 8000-8010:3000-3010 — maps a range of ports
  • UDP ports: docker run -p 53:53/udp dns-server
  • Binding to specific IP: docker run -p 127.0.0.1:8080:3000 — only accessible from localhost, not from other machines on the network
  • Using --publish-all: docker run -P my-image — Docker auto-publishes all exposed ports to random host ports (shortcut for -p on every EXPOSEd port)

Troubleshooting & edge cases

"Port already in use" error

docker run -p 80:80 nginx
# Error: driver failed programming external connectivity... Bind for 0.0.0.0:80 failed: port is already allocated

Fix: Stop the existing container using port 80 (docker ps -> docker stop <container>), or choose a different host port like 8080:80.

Wrong container port in mapping

You expose EXPOSE 4000 but your app listens on port 3000. The mapping -p 8080:4000 fails — the host reaches port 4000 inside the container, but nothing is listening there.

Fix: Match the container-side port in -p to the actual port your application binds to, not necessarily the EXPOSEd port.

Port is open, but connection still fails

Common causes: - Your app binds to 127.0.0.1 inside the container instead of 0.0.0.0 — then it won't accept connections from the Docker bridge network. Fix: change the listen address to 0.0.0.0. - Firewall rules on the host (iptables, ufw) blocking the host port. Fix: check sudo iptables -L -n and ensure your host port is allowed. - Container is using UDP but you're trying TCP. Fix: add /udp to the port mapping.

Port 0.0.0.0 vs 127.0.0.1 after binding

# Accessible only from localhost
docker run -p 127.0.0.1:8080:3000 port-demo
# Accessible from any interface (less secure)
docker run -p 0.0.0.0:8080:3000 port-demo

Pro tip: For development, binding to 127.0.0.1 is safer — it prevents other machines on your network from hitting your container.

What you learned & what's next

You now understand the critical difference between EXPOSE (documentation) and -p (actual publishing). You can: - Declare ports in a Dockerfile with EXPOSE to communicate intent - Map host ports to container ports using -p or --publish - Troubleshoot common port-related failures - Choose the right publishing strategy for development vs. production

This foundation directly enables the next lesson: Multi-container networking with Docker Compose. In that lesson, you'll see how ports interact with containers that need to talk to each other, and how EXPOSE simplifies Compose file configuration.

Practice recap

Try building a simple Flask or Go app that listens on port 5000, create a Dockerfile with EXPOSE 5000, and run it with -p 5001:5000. Access http://localhost:5001 to confirm it works. Then stop it and try docker run -P — use docker port to discover which host port Docker assigned.

Common mistakes

  • Forgetting to publish ports with -p and assuming EXPOSE alone makes the container accessible from the host.
  • Mapping the host port correctly but forgetting to listen on 0.0.0.0 inside the container — app binds to 127.0.0.1 and rejects external traffic.
  • Using -p with the wrong container-side port — mapping 8080:3000 while the app listens on port 4000.
  • Omitting the -p flag but trying to connect to the container's internal IP directly — host-to-container communication always requires -p or --link.

Variations

  1. Use docker run -P (capital P) to auto-publish all exposed ports to random host ports — useful for quick testing.
  2. In Docker Compose, define ports under services.<service>.ports using the same "host:container" format.
  3. For advanced networking, attach containers to the host network (--network host) and skip -p — the container uses the host's ports directly.

Real-world use cases

  • Publishing a web API container (-p 3000:3000) during local development so you can test with CURL or Postman.
  • Running a production database container (e.g., PostgreSQL) mapped to 127.0.0.1:5432:5432 to allow only localhost connections.
  • Deploying a multi-service app where one container (NGINX) maps ports 80 and 443 to host, while backend containers communicate via internal Docker networking without -p.

Key takeaways

  • EXPOSE is metadata that documents what ports a container listens on — it does not publish them.
  • The -p flag creates a tunnel from a host port to a container port, enabling external access.
  • Always match the container-side port in -p to the port your application actually binds to (e.g., -p 8080:3000 if your app uses 3000).
  • Use -p 127.0.0.1:host_port:container_port to restrict access to localhost only (more secure).
  • The docker port command shows active port mappings — great for debugging.
  • Inter-container communication on the same user-defined bridge network does not require -p.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.