Forward Host Port to Docker Container

Docker containers run in isolated network environments by default, which means services running inside containers aren't accessible from your host machine or the outside world without explicit port forwarding. Understanding how to map ports between your host and containers is essential for accessing web applications, databases, APIs, and other containerized services.
This guide covers port publishing from the basic -p flag to the cases that cause the most questions: adding a port to a container that already runs, ports vs expose in Compose, connecting to a database in a container, finding a container's IP address, and letting containers from two Compose projects talk to each other.
TLDR
docker run -p 8080:80 nginxpublishes container port 80 on host port 8080, on every host interface.-p 127.0.0.1:8080:80publishes it on the host's loopback address only. Use it for databases and admin tools.- You cannot add or change a published port on an existing container. Create a new container with the port, or edit
ports:in Compose and rundocker compose up -d. - In Compose,
ports:opens a port on the host.expose:(andEXPOSEin a Dockerfile) only documents a port: containers on the same network can always reach each other. - Containers on the same user-defined network reach each other by name. Use a shared external network for two Compose projects.
Prerequisites
You need Docker installed and basic familiarity with running containers. Understanding of network ports and how services listen on ports will help.
Basic Port Forwarding
The -p flag maps a host port to a container port.
Syntax
docker run -p <host-port>:<container-port> <image>
Example - running Nginx:
# Map host port 8080 to container port 80
docker run -p 8080:80 nginx
Now access Nginx at http://localhost:8080 on your host machine. Inside the container, Nginx listens on port 80, but you access it via 8080 on your host.
The app inside the container must listen on 0.0.0.0 (all interfaces), not on 127.0.0.1. A server bound to the container's loopback address accepts no connections through the published port, and you get "connection reset" or an empty reply. Many development servers default to 127.0.0.1: start them with --host 0.0.0.0 or the equivalent.
Multiple Port Mappings
Map multiple ports with multiple -p flags:
# Map both HTTP and HTTPS
docker run -p 8080:80 -p 8443:443 nginx
Or forward a range of ports:
# Map ports 8080-8090 to 80-90
docker run -p 8080-8090:80-90 myapp
Both ranges must have the same length. A host range with a single container port works differently: Docker takes the first free host port in the range, so you can start several copies with the same command. Each mapping gets an IPv4 and an IPv6 entry:
The last command prints a container ID before the error: Docker created the container and then failed to start it, so docker ps -a still lists it. Remove it with docker rm same before you reuse the name.
In a Dockerfile, one EXPOSE line can list several ports, with /udp where needed. -P then publishes every exposed port, the ones from the base image included:
FROM nginx:1.30
EXPOSE 8443 9229/tcp 5353/udp
80/tcp comes from the nginx base image. TCP and UDP have separate port spaces, so 80/tcp and 5353/udp can both get host port 32768. These outputs are from Docker Engine 28.0.4.
Publishing All Exposed Ports
Images can declare ports with EXPOSE in their Dockerfile. Use -P (capital P) to automatically map all exposed ports to random high ports on the host:
docker run -P nginx
Check which ports were assigned:
docker port <container-name>
Output:
80/tcp -> 0.0.0.0:32768
80/tcp -> [::]:32768
Nginx's port 80 is mapped to host port 32768. docker port <container-name> 80 prints the mapping for one port, and docker ps shows the same in its PORTS column.
Binding to Specific Interfaces
By default, -p 8080:80 binds to all network interfaces (0.0.0.0 and ::), making the service accessible from other machines. Bind to specific interfaces for security:
Localhost Only
# Only accessible from the host machine
docker run -p 127.0.0.1:8080:80 nginx
Now http://localhost:8080 works, but external machines can't connect.
Specific IP Address
# Bind to specific network interface
docker run -p 192.168.1.100:8080:80 nginx
The service is only accessible via the specified IP address.
Published Ports and the Host Firewall
On Linux, Docker writes its own iptables rules for published ports. Those rules see the traffic before ufw or firewalld rules for the host, so a port published on 0.0.0.0 can be reachable from the internet even when ufw blocks it. Two fixes:
- Publish only on
127.0.0.1(or a private address) when the port is not for the public. - Add filter rules to the
DOCKER-USERchain, which Docker evaluates before its own rules. The Docker packet filtering docs show examples.
UDP Port Forwarding
Specify the protocol for UDP services:
# Forward UDP port
docker run -p 5353:53/udp dns-server
# Forward both TCP and UDP
docker run -p 8080:80/tcp -p 5353:53/udp myapp
Docker Compose Port Mapping
In compose.yaml, use the ports section:
services:
web:
image: nginx:1.30
ports:
- '8080:80'
- '8443:443'
database:
image: postgres:18
ports:
- '127.0.0.1:5432:5432'
app:
image: myapp
ports:
- '3000-3005:3000-3005'
Start services:
docker compose up -d
All port mappings are applied automatically. docker compose ps and docker compose port web 80 show what was published. Quote the mappings: YAML reads some unquoted values such as 22:22 as numbers in base 60.
Short vs Long Syntax
Docker Compose supports both syntaxes:
Short syntax:
ports:
- '8080:80'
- '127.0.0.1:5432:5432'
Long syntax (more explicit):
ports:
- target: 80 # Container port
published: '8080' # Host port
protocol: tcp
- target: 5432
published: '5432'
host_ip: 127.0.0.1
What Is the Difference Between ports and expose in Docker Compose?
ports: publishes a container port on the host, so your browser, psql or another machine can reach it. expose: does not publish anything. It records which ports the service listens on, for people and for tools. Containers on the same Compose network reach each other on any port, with or without expose:.
services:
proxy:
image: nginx:1.30
ports:
- '80:80' # reachable from the host and the network
- '443:443'
api:
image: myorg/api:1.4
expose:
- '8000' # documentation only: proxy reaches api:8000 anyway
db:
image: postgres:18
environment:
POSTGRES_PASSWORD: change-me
# no ports: other hosts cannot reach the database, containers on this network can
With an nginx config that sends requests to http://api:8000 (a location block with proxy_pass, mounted into the proxy), the proxy reaches the API and the API connects to db:5432, both by service name. Nothing except the proxy is reachable from other machines. On a Linux host, the host itself can still reach container addresses directly. That is the layout to aim for: publish the entry point, keep the rest internal.
What Is the Difference Between EXPOSE and --publish?
The same split exists outside Compose. EXPOSE in a Dockerfile, like expose: in a Compose file, only records which ports the app listens on. docker run --publish (-p), like ports:, is what opens a port on the host:
# Host port 8080 to container port 80
docker run -d -p 8080:80 nginx:1.30
# Only on the host's loopback address, not on every interface
docker run -d -p 127.0.0.1:8080:80 nginx:1.30
EXPOSE changes one thing at run time: docker run -P (--publish-all) publishes every exposed port to a random free host port. The nginx image exposes port 80:
Leaving EXPOSE out does not block anything. We started python -m http.server 9000 from the python:3.13-alpine image, which exposes no ports (ExposedPorts is null), and another container on the same network still got 200 from http://api:9000/. Publish only the ports that people outside the host need. On Linux, Docker writes its own firewall rules for published ports, so a port published on 0.0.0.0 can be reachable even when ufw blocks it; bind it to 127.0.0.1 when only the host should reach it.
Different Ports for Development and Production
Keep the production file minimal, and publish extra ports for debugging in an override file that only developers use:
# compose.override.yaml: merged automatically by "docker compose up"
services:
db:
ports:
- '127.0.0.1:5432:5432' # local psql and GUI tools
docker compose up reads compose.yaml and compose.override.yaml together. In production, run docker compose -f compose.yaml up -d to leave the override out, or keep a separate compose.prod.yaml and pass both files with -f.
How Do I Add a Port to a Running Container?
You cannot. Docker sets up port publishing when it creates the container, and there is no command to add or change a mapping later. docker update does not change ports either. The fix is to create a new container with the port:
# 1. Look up the current settings, so the new container matches
docker inspect -f '{{.Config.Image}} {{json .HostConfig.PortBindings}} {{json .Mounts}}' web
# 2. Replace the container. Data in volumes stays; data in the writable layer does not.
docker stop web
docker rename web web-old
docker run -d --name web -p 8080:80 -v site-data:/usr/share/nginx/html nginx:1.30
# 3. When the new one works, remove the old one
docker rm web-old
With Compose, add the mapping under ports: and run docker compose up -d. Compose notices the changed config and recreates that one service.
If the old container holds data in its writable layer that you need, copy it out before you remove it (add a volume to an existing container shows how). docker commit web web-snapshot also keeps the files and the container's config, but not the contents of its volumes, and the result is an image nobody can rebuild. Use it only as a last resort.
A temporary forward without recreating anything. Run a small proxy container on the same network as the target and publish the port on the proxy:
# Find the target's network (the default bridge is called "bridge")
docker inspect -f '{{range $name, $net := .NetworkSettings.Networks}}{{$name}} {{end}}' web
# On a user-defined network, forward host port 8080 to web:80 by name
docker run -d --name web-forward --network mynet -p 8080:8080 \
alpine/socat TCP-LISTEN:8080,fork,reuseaddr TCP:web:80
Containers on the default bridge network have no DNS names. Use the target's IP there (see find a container's IP), or connect the target to a user-defined network first with docker network connect mynet web. You cannot publish a port on a container that uses --network container:web: Docker rejects port publishing in that network mode, so the proxy needs its own network namespace as above. Remove the proxy when you have recreated the container properly.
A reverse proxy such as nginx, Caddy or Traefik in front of your containers is the long-term version of this idea: one published port, and routes to containers on internal networks. The Nginx vs Traefik comparison helps you choose between two common ones.
How Do I Connect to PostgreSQL in a Docker Container From the Host?
Publish port 5432 when you start the container, then connect to localhost:
docker run -d --name pg \
-e POSTGRES_PASSWORD=change-me \
-e POSTGRES_DB=appdb \
-p 127.0.0.1:5432:5432 \
-v pgdata:/var/lib/postgresql \
postgres:18
psql "postgresql://postgres:change-me@localhost:5432/appdb" -c 'select version();'
The same in Compose:
services:
db:
image: postgres:18
environment:
POSTGRES_PASSWORD: change-me
POSTGRES_DB: appdb
ports:
- '127.0.0.1:5432:5432'
volumes:
- pgdata:/var/lib/postgresql
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
retries: 5
volumes:
pgdata:
You do not need to change listen_addresses or pg_hba.conf for this. The official image already listens on all addresses inside the container and accepts password logins from other hosts, which is why -p alone is enough. It is also why a port published on 0.0.0.0 is a risk: anyone who can reach the host can try passwords. Keep 127.0.0.1 unless another machine must connect.
When another machine must connect:
- Publish on an address that machine can reach:
-p 5432:5432, or-p 10.0.0.5:5432:5432for one private interface. - Allow only the client's network. Remember that
ufwrules do not filter published ports (see the firewall section), so filter inDOCKER-USERor in your cloud firewall. - To limit logins inside Postgres too, mount your own
pg_hba.confoutside the data directory and point the server at it:
cat > pg_hba.conf <<'EOF'
# TYPE DATABASE USER ADDRESS METHOD
local all all trust
host all all 127.0.0.1/32 scram-sha-256
host all all 10.0.0.0/24 scram-sha-256
EOF
docker run -d --name pg -e POSTGRES_PASSWORD=change-me -p 10.0.0.5:5432:5432 \
-v pgdata:/var/lib/postgresql \
-v "$(pwd)/pg_hba.conf":/etc/postgresql/pg_hba.conf:ro \
postgres:18 -c hba_file=/etc/postgresql/pg_hba.conf
Do not bind-mount the file into the data directory: on the first start the image must create the database cluster in an empty directory.
Common problems:
password authentication failedafter you changedPOSTGRES_PASSWORD. The variable only applies when the image creates a new cluster. With an existing volume, the old password stays. Change it withALTER USER postgres PASSWORD '...', or remove the volume if the data does not matter.- Port 5432 is already in use. A Postgres on the host often has it. Publish another host port,
-p 127.0.0.1:15432:5432, and connect to port 15432. - Connection refused right after start. The first start initializes the cluster, which takes a few seconds. Wait for
database system is ready to accept connectionsindocker logs pg, or for the health check. - From another container, do not publish at all. Put both on one user-defined network and connect to
pg:5432by name.
How Do I Get a Docker Container's IP Address From the Host?
Ask docker inspect for the address on each network the container is attached to:
docker inspect -f '{{range $name, $net := .NetworkSettings.Networks}}{{$name}}: {{$net.IPAddress}}{{"\n"}}{{end}}' web
# bridge: 172.17.0.2
Things to know before you use that address:
- It works from a Linux Docker host. On Docker Desktop (macOS and Windows), the containers run inside a VM, and their IPs are not reachable from your Mac or PC. Publish a port instead.
- It changes. A container gets a new address when it is recreated, and often after a restart. Do not put it in config files.
- Other machines cannot use it. The address belongs to a bridge network on the host, and by default Docker does not let other hosts reach container addresses directly. They can only reach ports published on the host's addresses.
- Other containers on the same network can use it, but a name is better: on a user-defined network, Docker's DNS resolves container names and Compose service names.
docker network create mynet
docker run -d --name api --network mynet myorg/api:1.4
docker run --rm --network mynet alpine:3.24 wget -qO- http://api:8000/health
For the reverse direction, from a container to a service on the host, see connect to the host's localhost from a container and get the host's IP from inside a container.
How Do Containers in Different Compose Projects Talk to Each Other?
Each Compose project gets its own default network, so the services of project A cannot resolve the services of project B. Attach both to one shared network. Create it once:
docker network create shared
Then declare it as external in both Compose files, and attach the services that must talk:
# project-a/compose.yaml
services:
app:
image: myorg/app:2.0
networks:
- default # keep the project's own network too
- shared
environment:
DB_HOST: db # the service name in project B
networks:
shared:
external: true
# project-b/compose.yaml
services:
db:
image: postgres:18
environment:
POSTGRES_PASSWORD: change-me
networks:
- default
- shared
networks:
shared:
external: true
Run docker compose up -d in each directory. app now reaches db:5432 by its service name. Check it with docker compose exec app getent hosts db, or with ping/nslookup if the image has them.
external: truemeans Compose does not create or delete the network.docker compose upfails withnetwork shared declared as external, but could not be founduntil you create it, anddocker compose downleaves it in place.- Service names can collide. If both projects have a service called
dbon the shared network, the name resolves to both. Give one a unique alias on the shared network:
services:
db:
networks:
shared:
aliases:
- billing-db
- An existing container joins with
docker network connect shared <container>, without a restart.
Checking Port Mappings
View port mappings for a running container:
# Using docker ps
docker ps
# Using docker port
docker port <container-name>
# Using docker inspect
docker inspect -f '{{json .NetworkSettings.Ports}}' <container-name>
Example output:
{ "80/tcp": [{ "HostIp": "0.0.0.0", "HostPort": "8080" }] }
Troubleshooting Port Conflicts
Port Already in Use
Docker reports a busy port in one of two ways:
Bind for 0.0.0.0:8080 failed: port is already allocated
failed to bind host port for 0.0.0.0:8080:172.17.0.2:80/tcp: address already in use
The first means another container publishes that port: docker ps --filter publish=8080 finds it. The second means a process on the host uses it. Find what's using the port:
# Linux
sudo ss -tlnp | grep :8080
# Linux and macOS
sudo lsof -i :8080
Solutions:
Use a different host port:
docker run -p 8081:80 nginxStop the conflicting service:
sudo systemctl stop apache2 docker run -p 80:80 nginx
Container Port Not Responding
Check that the service inside the container listens on the port you published, and on 0.0.0.0:
# Many slim images have no ss or netstat: borrow them from a debug container
docker run --rm --network container:mycontainer nicolaka/netshoot ss -tlnp
# Send a request from inside the container's network namespace
docker run --rm --network container:mycontainer nicolaka/netshoot curl -sI http://localhost:80
If the process listens on 127.0.0.1:80 only, change its bind address. If it does not listen at all, read docker logs mycontainer. Missing tools in a container explains the debug-container trick.
Reachable Locally but Not From Other Machines
Check the address in docker port: 127.0.0.1:8080 is local by design. For 0.0.0.0, check the cloud firewall or security group and the DOCKER-USER chain. Opening the port in ufw has no effect on published ports, as explained above.
Docker Networks and Port Forwarding
Containers on the same Docker network can communicate without port forwarding:
# Create a network
docker network create mynetwork
# Run database (no port forwarding needed)
docker run -d --network mynetwork --name db -e POSTGRES_PASSWORD=change-me postgres:18
# Run app (connects to 'db' by name)
docker run -d --network mynetwork --name app -p 8080:80 myapp
Inside myapp, connect to the database at postgresql://db:5432 - no port forwarding needed because they're on the same network. This name resolution only works on user-defined networks, not on the default bridge network.
Host Network Mode
Skip Docker networking entirely and use the host's network:
docker run --network host nginx
In host mode:
- No port mapping needed, and
-pis ignored - Container uses host ports directly
- Service on container port 80 is accessible at host port 80
- Less isolation but simpler networking
Use case: tools that must see the host's interfaces, or when the NAT step for published ports costs too much. Host networking is a Linux feature. On Docker Desktop it is limited, so check the Docker Desktop docs before you rely on it.
Best Practices
Don't expose databases publicly: Always bind database ports to localhost:
docker run -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=change-me postgres:18
Ports below 1024: the normal Docker daemon runs as root, so docker run -p 80:80 nginx works without sudo for any user who may use Docker. Rootless Docker cannot publish ports below 1024 unless you change the net.ipv4.ip_unprivileged_port_start sysctl.
Document your port mappings: In your README or Compose file comments:
services:
app:
ports:
- '3000:3000' # Web UI
- '127.0.0.1:9229:9229' # Node.js debugger, local only
Publish one entry point: put a reverse proxy on 80 and 443, and keep the other services on internal networks.
Plan your port allocation: Maintain a list of ports used by different containers to avoid conflicts.
Port forwarding is fundamental to using Docker effectively. Use -p for basic mapping, bind sensitive services like databases to localhost only, use networks and service names between containers, and remember that a new port means a new container.
Related Resources
- Docker: connect to the host's localhost from a container: the reverse direction
- How to access a host port from a container: host access patterns
- Introduction to Docker: Networking: drivers, DNS and internal networks
- Packet journey simulator: follow a request through NAT and routing
- Docker terminal simulator: practice
-p,docker portand networks - Docker security checklist: what to check before you publish a port
Try it hands-on
Run the commands from this article in the browser. Nothing to install.
Docker Terminal Simulator
Practice Docker commands in an interactive browser terminal. Learn images, containers, logs, exec, Dockerfile builds, volumes, networks, and Docker Compose with a live Docker daemon visualization.
Container Security Challenge
Six real docker run commands, each carrying a flag that hands an attacker the host. Click the dangerous ones, then see the actual escape path and what to grant instead. Covers the Docker socket, --privileged, host PID namespace, SYS_ADMIN and unconfined AppArmor, plus one command with nothing wrong with it.
We earn commissions when you shop through the links below.
Bitrise
Managed runners for GitHub Actions
Bitrise Build Hub runs your GitHub Actions jobs on M4 Macs and AMD Linux machines. Change runs-on and keep your workflows, secrets and logs on GitHub.
Atomsized
AWS platform engineering and GitOps
Design and automation for reliable AWS and Kubernetes platforms, safer delivery workflows, and preview and UAT environments your engineers can understand and own.
DigitalOcean
Cloud infrastructure for developers
Simple, reliable cloud computing designed for developers
DevDojo
Developer community & tools
Join a community of developers sharing knowledge and tools
SMTPfast
Developer-first email API
Send transactional and marketing email through a clean REST API. Detailed logs, webhooks, and embeddable signup forms in one dashboard.
QuizAPI
Developer-first quiz platform
Build, generate, and embed quizzes with a powerful REST API. AI-powered question generation and live multiplayer.
Want to support DevOps Daily and reach thousands of developers?
Become a SponsorTags
Found an issue?
Related Posts
Also worth your time on this topic
How to Get the IP Address of the Docker Host from Inside a Docker Container
Need your container to talk to the Docker host? Learn practical ways to discover the host's IP address from inside a container, with examples for Linux, macOS, and Windows.
Running Docker Containers on Your Linux Server
Install Docker and Docker Compose on Ubuntu, run your first container, deploy a WordPress stack with docker-compose, and set up Nginx as a reverse proxy in front of your containers.
60 minutes
Docker Security Hardening Checklist
Comprehensive security checklist for hardening Docker containers, images, and runtime environments.
60-90 minutes