Ports and service access
- Lesson 7
- 40 min
- Module 3: Networking, configuration and persistence
Lesson 7 of 15 · Docker and Cloud Essentials for AI DevelopersPorts and service access
1. Understand containers
2. Build the application image
3. Networking, configuration and persistence
4. Cloud building blocks
5. Deploy and clean up
What you will be able to do
Reach the ticket API in its container from your computer, and only from your computer. Learn to see why a container can run and still not answer, and let two containers talk to each other by name.
Before you startWhat to know and to have ready
What you need to know
- Ports and the address
127.0.0.1, from Browser-to-server basics: Ports: which program. - Why a server listens on
0.0.0.0, from Hosting bridge: Development mode and production mode. docker run,docker ps,docker logsanddocker exec, from Images and running containers: A first container and Worked example: a file in a container.
What you need on your computer
- Docker, running, from Install and verify: Check the installation.
- The project
~/projects/ticket-apiwith itsDockerfile, and the imageticket-api:1.0.0, from Runtime safety: Module practice. Check withdocker image ls ticket-api. - The image
python:3.14.8-slim-trixie, from Images and running containers: Get an image. If it is missing, Docker downloads it when you first use it. - For one step: a phone or a second computer on the same Wi-Fi network. If you have none, read the output in the lesson.
- If your project is missing or broken, copy the snapshot
end-of-m02, as Use a snapshot says. Then build the image in the project folder:docker build -t ticket-api:1.0.0 .
Tested with Docker Desktop 4.81.0 (Docker Engine 29.6.1) on macOS, Apple silicon. The Windows and Linux steps are not tested.
The app runs, but you cannot reach it
Section titled “The app runs, but you cannot reach it”Start with a small test. Amira runs the image with no extra options, except the API key:
docker run -d --name ticket-api -e API_KEY=local-dev-key ticket-api:1.0.0docker psThe output starts with the new container’s ID:
7afe3d39fe5b1f08c2e9474dafa7aef9acfa3902a2abf0e137a52da4dc7b4903CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES7afe3d39fe5b ticket-api:1.0.0 "fastapi run --host …" 3 seconds ago Up 3 seconds 8000/tcp ticket-apiThe container is up, and the API listens on port 8000. She calls it from her computer:
curl -sS http://127.0.0.1:8000/healthThe output:
curl: (7) Failed to connect to 127.0.0.1 port 8000 after 0 ms: Couldn't connect to serverThen she asks the same question from inside the container. The slim image has no curl, so she uses Python’s urllib:
docker exec ticket-api python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8000/health').read().decode())"The output:
{"status":"ok"}The app works. Only the way in is missing. A container has its own network: its own network addresses and its own ports, separate from your computer’s. Port 8000 in the container is not port 8000 on your computer. PORTS 8000/tcp in docker ps comes from the line EXPOSE 8000 in the Dockerfile. EXPOSE only documents the port. It does not open anything.
docker rm -f ticket-apiPublish a port
Section titled “Publish a port”To let your computer in, you publish a port when the container starts, with -p. A published port is a port on your computer that Docker forwards to a port in the container:
-p 127.0.0.1:8000:8000 └ host address └ host port └ container port- The container port is where the app listens: 8000, from the start command in the Dockerfile.
- The host port is the port that you use on your computer. It can be different, for example
8001. - The host address says which network address of your computer accepts the connection.
Amira runs the API again, with the port published:
docker run -d --name ticket-api -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0docker psdocker port ticket-apicurl -s http://127.0.0.1:8000/healthThe output:
721de8ac187e07bc74f72ee4b8a94b213c2dd56e73b349a88cf04778d23fd51bCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES721de8ac187e ticket-api:1.0.0 "fastapi run --host …" 3 seconds ago Up 3 seconds 127.0.0.1:8000->8000/tcp ticket-api8000/tcp -> 127.0.0.1:8000{"status":"ok"}127.0.0.1:8000->8000/tcp reads: “port 8000 of the address 127.0.0.1 on this computer goes to port 8000 in the container”. http://localhost:8000/health gives the same answer, because localhost is the name of 127.0.0.1.
The app’s log shows where the request came from:
docker logs --tail 2 ticket-apiThe output:
INFO ticket_api: GET /health 200 5ms id=338f1241cca9INFO: 172.17.0.1:58980 - "GET /health HTTP/1.1" 200 OKThe client is 172.17.0.1, not 127.0.0.1. From the app’s point of view, the request comes from Docker’s network, not from your computer’s own address. On another computer, the numbers can be different: you see an address like 172.17.0.1.
Choose who can connect
Section titled “Choose who can connect”The host address decides who can reach the published port. Amira tests it from another device. First she finds her computer’s address on the local network:
ipconfig getifaddr en0en0 is usually the Wi-Fi. If the command prints nothing, try en1.
ipconfigUse the IPv4 Address of your Wi-Fi adapter.
hostname -IUse the first address.
Her computer’s address was 10.0.0.109. From a second device on the same network, she opens http://10.0.0.109:8000/health. The result, here with curl from that address:
curl: (7) Failed to connect to 10.0.0.109 port 8000 after 8 ms: Couldn't connect to serverThen she starts a second container, with no host address in -p, on host port 8001:
docker run -d --name ticket-api-open -p 8001:8000 -e API_KEY=local-dev-key ticket-api:1.0.0docker ps --filter name=ticket-api-opencurl -s http://10.0.0.109:8001/healthThe output:
bd5e48b1b1def0fc2b29ed84076bd91fa2304121002a480e8c16d09c28d290a9CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESbd5e48b1b1de ticket-api:1.0.0 "fastapi run --host …" 3 seconds ago Up 3 seconds 0.0.0.0:8001->8000/tcp, [::]:8001->8000/tcp ticket-api-open{"status":"ok"}0.0.0.0 (and [::], the same for IPv6) means every network address of this computer. Any device that can reach your computer can now call the API. On a café’s Wi-Fi, that is everyone in the café.
-p option |
docker ps shows |
Who can connect |
|---|---|---|
-p 127.0.0.1:8000:8000 |
127.0.0.1:8000->8000/tcp |
Only programs on this computer |
-p 8001:8000 |
0.0.0.0:8001->8000/tcp, [::]:8001->8000/tcp |
Every device that can reach this computer |
no -p |
8000/tcp |
Nobody outside the container’s network |
This course always writes -p 127.0.0.1:.... Leave out the address only when other devices must connect, and only on a network that you trust.
Amira removes the open container:
docker rm -f ticket-api-openThe address inside the container
Section titled “The address inside the container”The host address is one half of the path. The other half is the address that the app listens on inside the container. The Dockerfile starts the app with fastapi run --host 0.0.0.0 --port 8000. What if it said --host 127.0.0.1, as fastapi dev and uvicorn do by default?
Predict: the container runs, and the port is published. What does curl show?
Amira removes the working container with docker rm -f ticket-api. Then she starts the image with a different command at the end of docker run. That command replaces the image’s CMD:
docker run -d --name ticket-api -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0 fastapi run --host 127.0.0.1 --port 8000docker pscurl -sS http://localhost:8000/healthThe output:
af205d8ff12517ec100dfb4adebfed5b3bc3aaff4d44aca93c88de17933fdac7CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESaf205d8ff125 ticket-api:1.0.0 "fastapi run --host …" 4 seconds ago Up 3 seconds 127.0.0.1:8000->8000/tcp ticket-apicurl: (52) Empty reply from serverEverything in docker ps looks right, but the reply is empty. The log shows the cause:
docker logs ticket-apiThe output (the last line):
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)Inside the container, 127.0.0.1 is the container itself, not your computer. The app accepts only connections that start inside the container. Docker’s forwarded connection arrives at the container’s network address, for example 172.17.0.2, and nobody listens there. A test from inside the container still works, so docker exec alone does not find this problem.
Text description of the diagram
On your computer, curl calls 127.0.0.1 port 8000. That is the published port. The Wi-Fi address 10.0.0.109 has no port 8000, because -p chose 127.0.0.1. Docker forwards the connection to the container’s own network address, 172.17.0.2, port 8000. The container also has its own 127.0.0.1, which only programs in the container can use. fastapi run --host 0.0.0.0 listens on both addresses of the container, so the forwarded connection arrives. With --host 127.0.0.1, it listens only on the container’s 127.0.0.1, and the forwarded connection finds nobody.
Text description of the diagram
curl on your computer sends GET /health to the published port. Docker forwards it to the container’s address, 172.17.0.2, port 8000. If the app listens on 0.0.0.0, it answers 200 {"status":"ok"}, and Docker passes the answer back. If the app listens on 127.0.0.1, nothing listens on 172.17.0.2. Docker closes the connection without an answer, and curl shows curl: (52) Empty reply from server.
Amira removes the broken container:
docker rm -f ticket-apilocalhost means this container
Section titled “localhost means this container”The same rule works in the other direction. A program in a container that calls localhost calls its own container. Amira starts the API again with the normal command, then calls it from a second container:
docker run -d --name ticket-api -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0docker run --rm python:3.14.8-slim-trixie python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/health').read().decode())"The output (the last line):
urllib.error.URLError: <urlopen error [Errno 111] Connection refused>The second container has no app on its own port 8000, so the connection is refused.
To call a program on your computer from a container, Docker Desktop gives the special name host.docker.internal. With http://host.docker.internal:8000/health, the same command printed {"status":"ok"} here. On Linux, Docker Engine needs an extra option for this name (--add-host=host.docker.internal:host-gateway); this course did not test it.
Container to container: a user-defined network
Section titled “Container to container: a user-defined network”Two containers that work together, such as the API and a database, need a better way than a path through your computer. Put them on the same user-defined network. That is a network that you create and name. On it, Docker gives each container a name that the others can look up: its container name.
First, the default network. Every container that you start without --network joins it. Amira calls the API by its name:
docker run --rm python:3.14.8-slim-trixie python -c "import urllib.request; print(urllib.request.urlopen('http://ticket-api:8000/health').read().decode())"The output (the last line):
urllib.error.URLError: <urlopen error [Errno -2] Name or service not known>On the default network, names do not work. Amira removes the container with docker rm -f ticket-api. Then she makes a network, and starts the API on it:
docker network create ticket-netdocker run -d --name ticket-api --network ticket-net -e API_KEY=local-dev-key ticket-api:1.0.0docker run --rm --network ticket-net python:3.14.8-slim-trixie python -c "import urllib.request; print(urllib.request.urlopen('http://ticket-api:8000/health').read().decode())"docker logs --tail 1 ticket-apiThe output:
6b3a99b0f573fcdcc2eada873a3281c43a8e7f3f499e0fd889a3f98e590fbed7859b97b8df79a958829450eea324a06347cda25073a2759a6d9b230d6d79c030{"status":"ok"}INFO: 172.21.0.3:54638 - "GET /health HTTP/1.1" 200 OKThe first two lines are the IDs of the new network and the new container. The name ticket-api worked, and the request came from the second container’s address on ticket-net, 172.21.0.3. The API needed no -p: containers on the same network reach each other’s container ports directly. A published port is only for your computer.
In the next lessons, Docker Compose makes such a network for you, and the API finds its database with the name db.
Amira cleans up:
docker rm -f ticket-apidocker network rm ticket-netGuided practice: reach the API
Section titled “Guided practice: reach the API”-
Open a terminal in
~/projects/ticket-api. -
Start the API with the port published on
127.0.0.1:Terminal window docker run -d --name ticket-api -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0If Docker says
Bind for 127.0.0.1:8000 failed: port is already allocated, another program uses port 8000. Rundocker psto find a container that uses it, and remove it. Then remove the failed container withdocker rm ticket-api, and try again. -
Run
docker ps. Check thatPORTSshows127.0.0.1:8000->8000/tcp. -
Call the API:
curl -s http://127.0.0.1:8000/health. Expect{"status":"ok"}. -
Find your computer’s network address, as in Choose who can connect. Call
http://<that address>:8000/healthfrom a phone or another computer on the same Wi-Fi. Expect no connection. -
Run
docker logs --tail 2 ticket-api. Find the client’s address of your request from step 4. -
Remove the container:
docker rm -f ticket-api.
Show the expected results
Step 3: 127.0.0.1:8000->8000/tcp in the PORTS column. Step 4: {"status":"ok"}. Step 5: the phone’s browser says that it cannot connect, because the port listens only on 127.0.0.1. Step 6: a line like INFO: 172.17.0.1:58980 - "GET /health HTTP/1.1" 200 OK. The address is Docker’s, not 127.0.0.1. The numbers can be different on your computer. Step 7: Docker prints ticket-api.
Your turn: a different host port
Section titled “Your turn: a different host port”Change one condition: publish the API on host port 9000, and put it on a user-defined network called ticket-net. Before you run anything, write down two URLs: the one that works from your computer, and the one that works from a second container on ticket-net. Then test both, and clean up.
Show the answer
docker network create ticket-netdocker run -d --name ticket-api --network ticket-net -p 127.0.0.1:9000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0curl -s http://127.0.0.1:9000/healthdocker run --rm --network ticket-net python:3.14.8-slim-trixie python -c "import urllib.request; print(urllib.request.urlopen('http://ticket-api:8000/health').read().decode())"docker rm -f ticket-apidocker network rm ticket-netFrom your computer: http://127.0.0.1:9000/health, the host port. From the second container: http://ticket-api:8000/health, the container port. The host port is only on your computer. A call from the second container to http://ticket-api:9000/health gives Connection refused, because nothing in the API container listens on 9000.
Common mistake
The container runs, but curl says: Empty reply from server
Symptom: docker ps shows the container as Up, with the port published. curl to the published port prints curl: (52) Empty reply from server.
Cause: Docker accepted the connection on your computer, but nothing answered at the other end in the container. Three causes give this same symptom:
- The app listens on
127.0.0.1inside the container, not on0.0.0.0. - The container port in
-pis wrong. For example,-p 127.0.0.1:8000:80forwards to port 80, but the app listens on 8000. This also gave(52)here. - The app is still starting. A request in the first second after the start can get
(52). Wait, and try again.
Fix:
- Read the start line in
docker logs <container>:Uvicorn running on http://0.0.0.0:8000is right.http://127.0.0.1:8000is the bind problem. Change the start command to--host 0.0.0.0. - Compare the port in that line with the second number in
docker port <container>. They must be the same. - If both are right, wait a few seconds and call again.
For a real AI service: model servers and API frameworks often listen on 127.0.0.1 by default, for safety on a laptop. In a container on a cloud platform, the same default gives a service that runs but never answers. The platform’s health check fails, and the platform restarts the container again and again (Module 5).
Check your understanding
What is the difference between -p 127.0.0.1:8000:8000 and -p 8000:8000?
Show the answer
Both forward port 8000 on your computer to port 8000 in the container. With 127.0.0.1, only programs on your computer can connect. Without an address, Docker listens on every network address of your computer (0.0.0.0 and [::]). Then every device that can reach your computer can call the API, for example on the same Wi-Fi. On Linux, such a port can also pass the ufw firewall.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
A container starts the app with --host 127.0.0.1 and is published with -p 127.0.0.1:8000:8000. docker ps shows it Up. Predict what curl http://localhost:8000/health shows, and explain why.
Show the answer
curl: (52) Empty reply from server. Inside the container, 127.0.0.1 means the container itself, so the app accepts only connections from inside the container. Docker forwards the request to the container’s network address (for example 172.17.0.2), where nothing listens. The fix is --host 0.0.0.0 in the container’s start command.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Container B must call the API in container A. Why does http://localhost:8000 not work in container B, and what works instead?
Show the answer
In container B, localhost is container B itself, which has no API on port 8000. Put both containers on the same user-defined network (docker network create, then --network for both), and call container A by its name and its container port, for example http://ticket-api:8000. No published port is needed for this.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Recap
- A container has its own network.
EXPOSEonly documents a port.-p host-address:host-port:container-portpublishes it. -p 127.0.0.1:...lets in only your computer. Without the address, every device that can reach your computer can connect.- The app in the container must listen on
0.0.0.0. With127.0.0.1, the container runs, but curl gets(52) Empty reply from server. - Inside a container,
localhostis that container. Containers on a user-defined network find each other by name, on the container port.
Next, you give the container its settings and its secrets when it starts. The API stops with a clear message when a required setting is missing.
Next: Runtime configuration