<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sharma</title>
    <description>The latest articles on DEV Community by Sharma (@hostomize).</description>
    <link>https://dev.to/hostomize</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4101859%2F32133a07-f6cf-42c8-8ea7-620cf515e169.png</url>
      <title>DEV Community: Sharma</title>
      <link>https://dev.to/hostomize</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hostomize"/>
    <language>en</language>
    <item>
      <title>Docker ports explained with EXPOSE expose and ports</title>
      <dc:creator>Sharma</dc:creator>
      <pubDate>Sat, 19 Sep 2026 08:41:59 +0000</pubDate>
      <link>https://dev.to/hostomize/docker-ports-explained-with-expose-expose-and-ports-3a89</link>
      <guid>https://dev.to/hostomize/docker-ports-explained-with-expose-expose-and-ports-3a89</guid>
      <description>&lt;p&gt;A container can be running normally while your browser gets a connection error. The application may be listening on the right port inside the container, but nothing has published that port on the host.&lt;br&gt;
Docker uses three similar-looking settings here: EXPOSE in a Dockerfile, expose in Compose, and ports in Compose. The first two describe container ports. The third creates a host port mapping. None of them starts a web server or changes the port on which your application listens.&lt;br&gt;
This walkthrough uses a small HTTP service to show the difference. It assumes Docker Engine with the Compose plugin, ordinary bridge networking, and a terminal on the Docker host. Host networking, custom routing and Kubernetes are outside its scope. The examples are a disposable learning setup, not a production deployment.&lt;br&gt;
Three settings with different effects&lt;br&gt;
EXPOSE 8000 records an intended port in an image. You can inspect that metadata, but writing it does not publish port 8000 on the host. An application can also listen on a port that the Dockerfile never mentions. Docker’s &lt;a href="https://docs.docker.com/reference/dockerfile/#expose" rel="noopener noreferrer"&gt;Dockerfile reference&lt;/a&gt; explains this distinction and the separate -P option that publishes exposed ports to automatically assigned host ports.&lt;br&gt;
Compose expose declares container-side ports without creating a host mapping. It is not an access-control list: leaving a port out does not prevent another container on the same network from connecting to a listening service. See the &lt;a href="https://docs.docker.com/reference/compose-file/services/#expose" rel="noopener noreferrer"&gt;Compose service reference&lt;/a&gt;.&lt;br&gt;
Compose ports maps a host address and port to a container port. In 127.0.0.1:8080:8000, the host-side port is 8080 and the application-side port is 8000. The address restricts this mapping to the host’s IPv4 loopback interface.&lt;br&gt;
Build a small service without publishing it&lt;br&gt;
Create an empty directory for this exercise. Save these three files in it. The image tag selects a Python 3.13 Alpine image; tags can change, so a repeatable production build should pin a reviewed digest.&lt;br&gt;
Dockerfile&lt;br&gt;
FROM python:3.13-alpine&lt;br&gt;
WORKDIR /site&lt;br&gt;
COPY index.html .&lt;br&gt;
EXPOSE 8000&lt;br&gt;
CMD ["python", "-m", "http.server", "8000", "--bind", "0.0.0.0"]&lt;br&gt;
index.html&lt;/p&gt;

&lt;h1&gt;Container port 8000 is responding&lt;/h1&gt;

&lt;p&gt;compose.yaml&lt;br&gt;
services:&lt;br&gt;
  web:&lt;br&gt;
    build: .&lt;br&gt;
    expose:&lt;br&gt;
      - "8000"&lt;br&gt;
Python’s basic HTTP server keeps this example small. Do not use it to serve a real public application. The explicit 0.0.0.0 binding lets it accept connections through the container’s network interface; 127.0.0.1 inside the container would refer to that container alone.&lt;br&gt;
Run these commands from the directory containing the files:&lt;br&gt;
docker compose up -d --build&lt;br&gt;
docker compose ps&lt;br&gt;
docker compose logs web&lt;br&gt;
With a successful build, the service should be running. The port display may show 8000/tcp; that is not a host mapping. You have not configured anything like 127.0.0.1:8080-&amp;gt;8000/tcp yet.&lt;br&gt;
Reach it from another container&lt;br&gt;
Compose gives services on its default network names that other attached containers can resolve. Add a temporary client so the complete compose.yaml becomes:&lt;br&gt;
services:&lt;br&gt;
  web:&lt;br&gt;
    build: .&lt;br&gt;
    expose:&lt;br&gt;
      - "8000"&lt;br&gt;
  probe:&lt;br&gt;
    image: python:3.13-alpine&lt;br&gt;
    profiles: ["tools"]&lt;br&gt;
Request the page through the service name:&lt;br&gt;
docker compose run --rm probe python -c '&lt;br&gt;
import urllib.request&lt;br&gt;
with urllib.request.urlopen("&lt;a href="http://web:8000" rel="noopener noreferrer"&gt;http://web:8000&lt;/a&gt;", timeout=5) as r:&lt;br&gt;
    print(r.read().decode())&lt;br&gt;
'&lt;br&gt;
The expected response contains the heading from index.html. This is an expected result for the example, not a recorded benchmark. If the request fails, check that web is running before investigating publishing rules.&lt;br&gt;
Now remove the expose section, leaving web with only build: .. Run docker compose up -d --build and repeat the probe. It should still work. Containers sharing this user-defined bridge network can reach listening ports without an expose declaration. Docker documents the behaviour in its bridge network guide.&lt;br&gt;
The probe uses web:8000, not localhost:8000. Inside the probe, localhost means the probe itself. Docker’s &lt;a href="https://docs.docker.com/compose/how-tos/networking/" rel="noopener noreferrer"&gt;Compose networking guide&lt;/a&gt; covers service names and the difference between container and host ports.&lt;br&gt;
Publish a port for the host&lt;br&gt;
Replace compose.yaml with this version:&lt;br&gt;
services:&lt;br&gt;
  web:&lt;br&gt;
    build: .&lt;br&gt;
    ports:&lt;br&gt;
      - "127.0.0.1:8080:8000"&lt;br&gt;
  probe:&lt;br&gt;
    image: python:3.13-alpine&lt;br&gt;
    profiles: ["tools"]&lt;br&gt;
Apply it, inspect the mapping, and request the page from the Docker host:&lt;br&gt;
docker compose up -d --build&lt;br&gt;
docker compose port web 8000&lt;br&gt;
curl --fail &lt;a href="http://127.0.0.1:8080" rel="noopener noreferrer"&gt;http://127.0.0.1:8080&lt;/a&gt;&lt;br&gt;
The mapping command should report 127.0.0.1:8080. The host uses port 8080; the probe continues to use web:8000. Publishing has not changed Python’s listening port.&lt;br&gt;
On a remote VPS, run that curl command in your VPS terminal. Opening the same URL on your laptop contacts the laptop. For local browser access, an SSH tunnel is one option:&lt;br&gt;
ssh -N -L 8080:127.0.0.1:8080 user@your-vps&lt;br&gt;
Replace the account and hostname, keep the tunnel open, and browse to &lt;a href="http://127.0.0.1:8080" rel="noopener noreferrer"&gt;http://127.0.0.1:8080&lt;/a&gt; on the laptop. Its local port 8080 must be free.&lt;br&gt;
Check the address before going public&lt;br&gt;
Changing the mapping to 8080:8000 normally publishes on all host interfaces. Whether someone can reach it then also depends on routing and firewall rules. Keep the loopback mapping for this exercise. Docker’s port &lt;a href="https://docs.docker.com/engine/network/port-publishing/" rel="noopener noreferrer"&gt;publishing documentation&lt;/a&gt; also notes that releases before 28.0.0 had a localhost-publishing caveat involving other machines on the same layer-2 network; use a supported, updated engine.&lt;br&gt;
On Linux, published Docker traffic can bypass the filtering path people expect from UFW. Review Docker’s &lt;a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" rel="noopener noreferrer"&gt;firewall documentation &lt;/a&gt;for your backend rather than assuming an ordinary host rule covers every published container. A real deployment needs verification from outside the server, including IPv6 where enabled.&lt;br&gt;
For an application behind a reverse proxy, the proxy’s location determines the connection. A proxy on the host can use a loopback mapping. A proxy in another container normally reaches the application through a shared Docker network and its service name. Databases used only by application containers generally do not need a published host port.&lt;br&gt;
When you are done, run docker compose down from this exercise directory. It removes the example containers and project network; the image and source files remain.&lt;br&gt;
I’m affiliated with Hostomize. If you still need the host setup, our guide to &lt;a href="https://hostomize.com/blog/installing-docker-on-vps/" rel="noopener noreferrer"&gt;installing Docker on a VPS&lt;/a&gt; covers that earlier step.&lt;br&gt;
Before adding another port mapping, name the client that needs it: another container, the host, or an external user. That answer usually tells you which address and port to configure.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
