Homelab Academy / Modules
Guided practice + break/fix + lab gate.
Module: Compose Operations & IaC
Activity type: Lab (Practice)
Lab risk: low
Objective: Explain the difference between a container port and a host-published port
/opt/lab-classroom/class79/
All files created or edited by this lab remain under /opt/lab-classroom/class79/. Docker will also create project-scoped containers and networks managed by the Docker daemon.
1
Confirm that Docker Engine and Docker Compose v2 are available.
docker version && docker compose version
2
Create the lab directories. Do not substitute another path.
mkdir -p /opt/lab-classroom/class79/gateway /opt/lab-classroom/class79/database && cd /opt/lab-classroom/class79
3
Create a response file for the gateway service.
cat > /opt/lab-classroom/class79/gateway/status.txt <<'EOF'
gateway reachable
EOF
4
Create a response file for the backend service.
cat > /opt/lab-classroom/class79/database/db.txt <<'EOF'
database reachable through backend network
EOF
5
Create the Compose definition. The client uses tail -f /dev/null because it is supported by Alpine BusyBox and remains available for later docker compose exec commands.
cat > /opt/lab-classroom/class79/compose.yaml <<'EOF'
name: class79
services:
client:
image: alpine:3.20
user: "65534:65534"
command: ["tail", "-f", "/dev/null"]
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
networks:
- frontend
gateway:
image: alpine:3.20
user: "65534:65534"
command: ["httpd", "-f", "-p", "8080", "-h", "/srv"]
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
- type: bind
source: ./gateway
target: /srv
read_only: true
networks:
- frontend
- backend
database:
image: alpine:3.20
user: "65534:65534"
command: ["httpd", "-f", "-p", "9090", "-h", "/srv"]
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
- type: bind
source: ./database
target: /srv
read_only: true
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true
EOF
6
Validate the rendered Compose configuration, start the services, and verify that all three services remain running.
cd /opt/lab-classroom/class79 && docker compose config && docker compose up -d && docker compose ps
7
From the client, request the gateway by Compose service name. Successful wget output verifies both service-name resolution and HTTP connectivity.
cd /opt/lab-classroom/class79 && docker compose exec -T client sh -c 'command -v wget >/dev/null || exit 90; wget -T 5 -qO- http://gateway:8080/status.txt'
8
From the gateway, request the database by Compose service name. This verifies the permitted backend path.
cd /opt/lab-classroom/class79 && docker compose exec -T gateway sh -c 'command -v wget >/dev/null || exit 90; wget -T 5 -qO- http://database:9090/db.txt'
9
Before the negative test, confirm that the client is running, wget exists, and the positive client-to-gateway control still succeeds.
cd /opt/lab-classroom/class79 && docker compose ps --status running --services && docker compose exec -T client sh -c 'command -v wget >/dev/null || exit 90; wget -T 5 -qO- http://gateway:8080/status.txt'
10
Attempt the intentionally disallowed client-to-database request. A nonzero result counts as evidence of isolation only when the diagnostic reports a name-resolution or connection-path failure. An exited container or missing executable is a lab failure, not a successful negative test.
cd /opt/lab-classroom/class79 && docker compose exec -T client sh -c 'command -v wget >/dev/null || exit 90; wget -T 3 -O- http://database:9090/db.txt'
11
Inspect network membership to obtain structural evidence for the permitted and denied paths.
docker inspect class79-client-1 --format '{{json .NetworkSettings.Networks}}' && docker inspect class79-gateway-1 --format '{{json .NetworkSettings.Networks}}' && docker inspect class79-database-1 --format '{{json .NetworkSettings.Networks}}'
12
Verify that gateway port 8080 is not published on the host. Because there is no ports mapping, this command is expected to report an error or produce no mapping.
cd /opt/lab-classroom/class79 && docker compose port gateway 8080
| Symptom | Likely cause | Fix |
|---|---|---|
| The client exits immediately after startup. | The client command was changed to an unsupported or finite command, such as sleep infinity on an Alpine BusyBox version that does not accept that operand. | Run docker compose logs client and docker inspect class79-client-1 --format '{{json .Config.Cmd}}'. Restore command: ["tail", "-f", "/dev/null"] in /opt/lab-classroom/class79/compose.yaml, then run docker compose up -d --force-recreate client. |
| One or more services repeatedly exit or are absent from docker compose ps. | The Compose file is invalid, the image is unavailable, a bind-mounted directory is missing, or the configured process failed. | Run docker compose config, then inspect docker compose ps -a and docker compose logs client gateway database from /opt/lab-classroom/class79. Correct only files in the lab workspace and recreate the affected service. |
| The client cannot retrieve the gateway response. | The gateway is not running, the frontend network attachment is missing, the service name or port is wrong, or the HTTP server has not started. | Confirm client and gateway are running, inspect both containers' network attachments, verify that both include class79_frontend, and confirm that the request uses http://gateway:8080/status.txt. |
| The gateway cannot retrieve the database response. | The gateway or database is not attached to backend, database is not running, or the request uses the wrong port or filename. | Inspect both containers and verify class79_backend membership. Check docker compose logs database and retry http://database:9090/db.txt from gateway. |
| The client can directly retrieve the database response. | The client was accidentally attached to backend, the database was attached to frontend, or an earlier container with different configuration is being tested. | Run docker compose config and inspect all three containers. Restore the intended network lists, run docker compose up -d --force-recreate, and confirm the project and container names begin with class79. |
| The client-to-database command fails with an executable-not-found message. | The image or command environment differs from the lesson, or wget is unavailable. | Do not count this as successful isolation. Confirm the service uses alpine:3.20 and run docker compose exec -T client sh -c 'command -v wget'. Recreate the client with the specified image if necessary. |
| docker compose port gateway 8080 reports that no public port exists. | No ports mapping is configured. | This is the expected result. The gateway is intended to be reachable only from containers on frontend, not from a host-published port. |
| Bind-mounted files return permission denied. | The host directories or files are not readable by container user 65534. | Inspect the ownership and read bits under /opt/lab-classroom/class79/. Ensure the directories are traversable and the two text files are readable without granting unnecessary write access. |
No container port is published to the host.
The client is not attached to the backend network.
The database is not attached to the frontend network.
The backend network is marked internal.
Containers run as numeric user 65534 rather than the default root user.
All Linux capabilities are dropped.
The no-new-privileges security option is enabled.
Container root filesystems and bind mounts are read-only.
Docker network segmentation is not a substitute for database authentication or authorization.
The gateway is intentionally able to reach the database and therefore becomes a security-sensitive trust boundary.
An internal Docker network limits external routing but does not prevent communication among its members.
Plain HTTP is used only for local connectivity demonstration and does not provide transport confidentiality or server identity.
Membership on the docker group commonly grants root-equivalent control over the Docker host; access to the Docker daemon must be tightly controlled.
Stop and remove only the class79 Compose containers and project networks. The lesson files remain under the approved workspace for review or reuse.
cd /opt/lab-classroom/class79 && docker compose down --volumes --remove-orphans
Run docker ps -a --filter name=class79 and docker network ls --filter name=class79. No class79 lab containers or project networks should remain.