diff --git a/labs/compose-service-dns-breaks-after-rename/cloud-init.yaml b/labs/compose-service-dns-breaks-after-rename/cloud-init.yaml new file mode 100644 index 0000000..40c66c3 --- /dev/null +++ b/labs/compose-service-dns-breaks-after-rename/cloud-init.yaml @@ -0,0 +1,47 @@ +#cloud-config +packages: + - docker.io + - docker-compose-plugin +runcmd: + - mkdir -p /opt/brokenops/compose-service-dns-breaks-after-rename/backend + - mkdir -p /opt/brokenops/compose-service-dns-breaks-after-rename/proxy + - | + cat <<'EOF' > /opt/brokenops/compose-service-dns-breaks-after-rename/backend/index.html +

BrokenOps backend is alive

+ EOF + - | + cat <<'EOF' > /opt/brokenops/compose-service-dns-breaks-after-rename/backend/Dockerfile + FROM python:3.11-alpine + WORKDIR /srv + COPY index.html /srv/index.html + CMD ["python3", "-m", "http.server", "8000", "--directory", "/srv"] + EOF + - | + cat <<'EOF' > /opt/brokenops/compose-service-dns-breaks-after-rename/proxy/index.html +

BrokenOps proxy is serving content

+

Upstream host is configured separately.

+ EOF + - | + cat <<'EOF' > /opt/brokenops/compose-service-dns-breaks-after-rename/proxy/Dockerfile + FROM python:3.11-alpine + WORKDIR /srv + COPY index.html /srv/index.html + ENV UPSTREAM_HOST=web + CMD ["python3", "-m", "http.server", "8080", "--directory", "/srv"] + EOF + - | + cat <<'EOF' > /opt/brokenops/compose-service-dns-breaks-after-rename/compose.yml + services: + backend: + build: ./backend + proxy: + build: ./proxy + ports: + - "8080:8080" + depends_on: + - backend + EOF + - chown -R root:root /opt/brokenops/compose-service-dns-breaks-after-rename + - docker rm -f brokenops-compose-backend brokenops-compose-proxy >/dev/null 2>&1 || true + - docker run -d --name brokenops-compose-backend -p 8000:80 -v /opt/brokenops/compose-service-dns-breaks-after-rename/backend/index.html:/usr/share/nginx/html/index.html:ro nginx:alpine >/tmp/brokenops-compose-backend.log 2>&1 || true + - docker run -d --name brokenops-compose-proxy -p 8080:80 -v /opt/brokenops/compose-service-dns-breaks-after-rename/proxy/index.html:/usr/share/nginx/html/index.html:ro nginx:alpine >/tmp/brokenops-compose-proxy.log 2>&1 || true diff --git a/labs/compose-service-dns-breaks-after-rename/lab.yaml b/labs/compose-service-dns-breaks-after-rename/lab.yaml new file mode 100644 index 0000000..1cd376e --- /dev/null +++ b/labs/compose-service-dns-breaks-after-rename/lab.yaml @@ -0,0 +1,16 @@ +id: "compose-service-dns-breaks-after-rename" +name: "Compose Service DNS Breaks After Rename" +category: "docker" +difficulty: "intermediate" +description: + summary: "A Compose proxy keeps an outdated upstream hostname after the backend was renamed." +vm: + name: "docker-compose-dns-lab" + memory: 2048 + cpu: 2 + disk: "10G" +cloud_init: "cloud-init.yaml" +verify_script: "verify.sh" +exposed_ports: + - 8080 +port_works_initially: true diff --git a/labs/compose-service-dns-breaks-after-rename/question.md b/labs/compose-service-dns-breaks-after-rename/question.md new file mode 100644 index 0000000..7dd56a8 --- /dev/null +++ b/labs/compose-service-dns-breaks-after-rename/question.md @@ -0,0 +1,11 @@ +### Scenario +A Compose-based proxy still references an old backend hostname after the service was renamed. + +### Objective +Fix the service name or network alias so the proxy can resolve and reach the backend container again. + +### Useful Commands +- `docker compose up -d --build` +- `docker compose logs proxy` +- `docker compose exec proxy getent hosts backend` +- `curl http://127.0.0.1:8080/` diff --git a/labs/compose-service-dns-breaks-after-rename/solution.md b/labs/compose-service-dns-breaks-after-rename/solution.md new file mode 100644 index 0000000..69be6d1 --- /dev/null +++ b/labs/compose-service-dns-breaks-after-rename/solution.md @@ -0,0 +1,19 @@ +### The Issue +The proxy container still tries to fetch content from `web`, but the Compose service is now named `backend`. + +### Step-by-Step Fix + +1. **Inspect the Compose stack**: + Review the service names and confirm the backend service is called `backend`. + +2. **Update the upstream hostname**: + Change the proxy configuration so it fetches from `backend:8000` instead of the stale name. + +3. **Rebuild and start the stack**: + Bring the Compose project back up after the DNS target is corrected. + +4. **Verify the proxy response**: + Curl the published port and confirm the backend HTML is returned. + +5. **Verify the fix**: + Once the proxy can resolve the backend and serve the page, the lab is complete. diff --git a/labs/compose-service-dns-breaks-after-rename/solution.sh b/labs/compose-service-dns-breaks-after-rename/solution.sh new file mode 100755 index 0000000..820be22 --- /dev/null +++ b/labs/compose-service-dns-breaks-after-rename/solution.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +cd /opt/brokenops/compose-service-dns-breaks-after-rename +sed -i 's/UPSTREAM_HOST=web/UPSTREAM_HOST=backend/' proxy/Dockerfile diff --git a/labs/compose-service-dns-breaks-after-rename/verify.sh b/labs/compose-service-dns-breaks-after-rename/verify.sh new file mode 100755 index 0000000..39b3499 --- /dev/null +++ b/labs/compose-service-dns-breaks-after-rename/verify.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail +cd /opt/brokenops/compose-service-dns-breaks-after-rename +if grep -q '^ENV UPSTREAM_HOST=backend$' proxy/Dockerfile; then + echo "SUCCESS: Compose uses the updated backend hostname." + exit 0 +fi +echo "FAILURE: The proxy Dockerfile still references the stale upstream hostname." +grep '^ENV UPSTREAM_HOST=' proxy/Dockerfile || true +exit 1 diff --git a/labs/docker-entrypoint-flags/cloud-init.yaml b/labs/docker-entrypoint-flags/cloud-init.yaml new file mode 100644 index 0000000..29a895d --- /dev/null +++ b/labs/docker-entrypoint-flags/cloud-init.yaml @@ -0,0 +1,26 @@ +#cloud-config +packages: + - docker.io +runcmd: + - mkdir -p /opt/brokenops/docker-entrypoint-flags + - | + cat <<'EOF' > /opt/brokenops/docker-entrypoint-flags/app.py + #!/usr/bin/env python3 + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('--host', required=True) + parser.add_argument('--port', required=True) + args = parser.parse_args() + print(f'ready on {args.host}:{args.port}') + EOF + - chmod +x /opt/brokenops/docker-entrypoint-flags/app.py + - | + cat <<'EOF' > /opt/brokenops/docker-entrypoint-flags/Dockerfile + FROM python:3.11-alpine + WORKDIR /app + COPY app.py /app/app.py + ENTRYPOINT ["python3", "/app/app.py"] + CMD ["--bind", "0.0.0.0", "--port", "8080"] + EOF + - chown -R root:root /opt/brokenops/docker-entrypoint-flags diff --git a/labs/docker-entrypoint-flags/lab.yaml b/labs/docker-entrypoint-flags/lab.yaml new file mode 100644 index 0000000..fda1bc1 --- /dev/null +++ b/labs/docker-entrypoint-flags/lab.yaml @@ -0,0 +1,13 @@ +id: "docker-entrypoint-flags" +name: "Container Entrypoint Uses Wrong Command Flags" +category: "docker" +difficulty: "beginner" +description: + summary: "A container exits immediately because the entrypoint passes outdated command-line flags." +vm: + name: "docker-entrypoint-lab" + memory: 2048 + cpu: 2 + disk: "10G" +cloud_init: "cloud-init.yaml" +verify_script: "verify.sh" diff --git a/labs/docker-entrypoint-flags/question.md b/labs/docker-entrypoint-flags/question.md new file mode 100644 index 0000000..7e9f806 --- /dev/null +++ b/labs/docker-entrypoint-flags/question.md @@ -0,0 +1,10 @@ +### Scenario +A container image builds, but the application exits right away because the default command still uses an obsolete flag name. + +### Objective +Update the container command so the application starts with the arguments it actually understands. + +### Useful Commands +- `docker build .` +- `docker run --rm ` +- `docker inspect ` diff --git a/labs/docker-entrypoint-flags/solution.md b/labs/docker-entrypoint-flags/solution.md new file mode 100644 index 0000000..9765cda --- /dev/null +++ b/labs/docker-entrypoint-flags/solution.md @@ -0,0 +1,19 @@ +### The Issue +The Dockerfile passes `--bind` to an app that expects `--host`. That mismatch makes the container exit before it can finish startup. + +### Step-by-Step Fix + +1. **Inspect the image command**: + Check the Dockerfile and confirm which flags the application script expects. + +2. **Fix the default command**: + Replace the obsolete flag with the one the script understands. + +3. **Rebuild the image**: + Build the image again so the corrected command is included. + +4. **Run the container**: + Confirm the container prints `ready on 0.0.0.0:8080` and exits successfully. + +5. **Verify the fix**: + The lab is solved once the container starts cleanly with the corrected flags. diff --git a/labs/docker-entrypoint-flags/solution.sh b/labs/docker-entrypoint-flags/solution.sh new file mode 100755 index 0000000..109a2ca --- /dev/null +++ b/labs/docker-entrypoint-flags/solution.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +cd /opt/brokenops/docker-entrypoint-flags +sed -i 's/--bind/--host/' Dockerfile diff --git a/labs/docker-entrypoint-flags/verify.sh b/labs/docker-entrypoint-flags/verify.sh new file mode 100755 index 0000000..8380b32 --- /dev/null +++ b/labs/docker-entrypoint-flags/verify.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail +cd /opt/brokenops/docker-entrypoint-flags +docker build -t brokenops-entrypoint-flags . >/tmp/brokenops-entrypoint-build.log 2>&1 +output=$(docker run --rm brokenops-entrypoint-flags) +if [[ "$output" == "ready on 0.0.0.0:8080" ]]; then + echo "SUCCESS: The container entrypoint starts with the correct flags." + exit 0 +fi +echo "FAILURE: The container did not start with the expected flags." +exit 1 diff --git a/labs/docker-loopback-bind/cloud-init.yaml b/labs/docker-loopback-bind/cloud-init.yaml new file mode 100644 index 0000000..89aa79e --- /dev/null +++ b/labs/docker-loopback-bind/cloud-init.yaml @@ -0,0 +1,24 @@ +#cloud-config +packages: + - docker.io +runcmd: + - mkdir -p /opt/brokenops/docker-loopback-bind + - | + cat <<'EOF' > /opt/brokenops/docker-loopback-bind/server.py + #!/usr/bin/env python3 + from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer + + server = ThreadingHTTPServer(('127.0.0.1', 8080), SimpleHTTPRequestHandler) + print('serving on 127.0.0.1:8080') + server.serve_forever() + EOF + - chmod +x /opt/brokenops/docker-loopback-bind/server.py + - | + cat <<'EOF' > /opt/brokenops/docker-loopback-bind/Dockerfile + FROM python:3.11-alpine + WORKDIR /app + COPY server.py /app/server.py + EXPOSE 8080 + CMD ["python3", "/app/server.py"] + EOF + - chown -R root:root /opt/brokenops/docker-loopback-bind diff --git a/labs/docker-loopback-bind/lab.yaml b/labs/docker-loopback-bind/lab.yaml new file mode 100644 index 0000000..710c040 --- /dev/null +++ b/labs/docker-loopback-bind/lab.yaml @@ -0,0 +1,16 @@ +id: "docker-loopback-bind" +name: "Container Port Binds Only to Loopback" +category: "docker" +difficulty: "intermediate" +description: + summary: "An exposed container port still fails because the service listens on 127.0.0.1 only." +vm: + name: "docker-loopback-lab" + memory: 2048 + cpu: 2 + disk: "10G" +cloud_init: "cloud-init.yaml" +verify_script: "verify.sh" +exposed_ports: + - 8080 +port_works_initially: false diff --git a/labs/docker-loopback-bind/question.md b/labs/docker-loopback-bind/question.md new file mode 100644 index 0000000..5fbc7ce --- /dev/null +++ b/labs/docker-loopback-bind/question.md @@ -0,0 +1,11 @@ +### Scenario +The container starts, but the Open Port path cannot reach it because the service only listens on localhost inside the container. + +### Objective +Change the bind address so the published port becomes reachable from outside the container. + +### Useful Commands +- `docker build .` +- `docker run -d -p 8080:8080 ` +- `curl http://127.0.0.1:8080/` +- `docker logs ` diff --git a/labs/docker-loopback-bind/solution.md b/labs/docker-loopback-bind/solution.md new file mode 100644 index 0000000..5623c00 --- /dev/null +++ b/labs/docker-loopback-bind/solution.md @@ -0,0 +1,19 @@ +### The Issue +The app binds to `127.0.0.1` inside the container, so Docker's port publishing cannot reach it. + +### Step-by-Step Fix + +1. **Inspect the listener**: + Check the server script and confirm it is binding to localhost only. + +2. **Change the bind address**: + Update the server to listen on `0.0.0.0` so Docker can forward traffic to it. + +3. **Rebuild and restart the container**: + Build the image again, start a new container, and publish port 8080. + +4. **Test the published port**: + Curl `http://127.0.0.1:8080/` from the VM and confirm the response comes back. + +5. **Verify the fix**: + The lab is solved when the Open Port path and local curl both work. diff --git a/labs/docker-loopback-bind/solution.sh b/labs/docker-loopback-bind/solution.sh new file mode 100755 index 0000000..6268fe7 --- /dev/null +++ b/labs/docker-loopback-bind/solution.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +cd /opt/brokenops/docker-loopback-bind +sed -i "s/127.0.0.1/0.0.0.0/" server.py diff --git a/labs/docker-loopback-bind/verify.sh b/labs/docker-loopback-bind/verify.sh new file mode 100755 index 0000000..bd3e177 --- /dev/null +++ b/labs/docker-loopback-bind/verify.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail +cd /opt/brokenops/docker-loopback-bind +docker build -t brokenops-loopback-bind . >/tmp/brokenops-loopback-build.log 2>&1 +docker rm -f brokenops-loopback-bind >/dev/null 2>&1 || true +docker run -d --name brokenops-loopback-bind -p 8080:8080 brokenops-loopback-bind >/dev/null +sleep 3 +if curl -fsS http://127.0.0.1:8080/ >/tmp/brokenops-loopback-response.txt; then + echo "SUCCESS: The container is reachable through the published port." + exit 0 +fi +echo "FAILURE: The published port still is not reachable." +docker logs brokenops-loopback-bind --tail 20 || true +exit 1 diff --git a/labs/docker-multi-stage-copy-path/cloud-init.yaml b/labs/docker-multi-stage-copy-path/cloud-init.yaml new file mode 100644 index 0000000..c120464 --- /dev/null +++ b/labs/docker-multi-stage-copy-path/cloud-init.yaml @@ -0,0 +1,15 @@ +#cloud-config +packages: + - docker.io +runcmd: + - mkdir -p /opt/brokenops/docker-multi-stage-copy-path + - | + cat <<'EOF' > /opt/brokenops/docker-multi-stage-copy-path/Dockerfile + FROM alpine:3.19 AS builder + RUN mkdir -p /out && echo 'BrokenOps multi-stage build' > /out/app.txt + + FROM alpine:3.19 + COPY --from=builder /out/app.tx /app/app.txt + CMD ["cat", "/app/app.txt"] + EOF + - chown -R root:root /opt/brokenops/docker-multi-stage-copy-path diff --git a/labs/docker-multi-stage-copy-path/lab.yaml b/labs/docker-multi-stage-copy-path/lab.yaml new file mode 100644 index 0000000..1a6f94b --- /dev/null +++ b/labs/docker-multi-stage-copy-path/lab.yaml @@ -0,0 +1,13 @@ +id: "docker-multi-stage-copy-path" +name: "Docker Multi-Stage COPY Path" +category: "docker" +difficulty: "intermediate" +description: + summary: "A multi-stage Docker build fails because the final stage copies from the wrong artifact path." +vm: + name: "docker-copy-lab" + memory: 2048 + cpu: 2 + disk: "10G" +cloud_init: "cloud-init.yaml" +verify_script: "verify.sh" diff --git a/labs/docker-multi-stage-copy-path/question.md b/labs/docker-multi-stage-copy-path/question.md new file mode 100644 index 0000000..0e55a6e --- /dev/null +++ b/labs/docker-multi-stage-copy-path/question.md @@ -0,0 +1,10 @@ +### Scenario +A container image stopped building after a refactor changed the artifact path in a multi-stage Dockerfile. + +### Objective +Fix the Dockerfile so the final image can be built and run successfully again. + +### Useful Commands +- `docker build .` +- `docker run --rm ` +- `sed -n '1,120p' Dockerfile` diff --git a/labs/docker-multi-stage-copy-path/solution.md b/labs/docker-multi-stage-copy-path/solution.md new file mode 100644 index 0000000..2ecf427 --- /dev/null +++ b/labs/docker-multi-stage-copy-path/solution.md @@ -0,0 +1,19 @@ +### The Issue +The final Docker stage is copying the built artifact from the wrong path. The builder creates `/out/app.txt`, but the final stage tries to copy `/out/app.tx`. + +### Step-by-Step Fix + +1. **Inspect the Dockerfile**: + Read the multi-stage build and confirm the artifact name in the builder stage. + +2. **Correct the `COPY` source path**: + Update the final stage so it copies `/out/app.txt` into the runtime image. + +3. **Rebuild the image**: + Build the image again and make sure the Docker build completes without errors. + +4. **Run the container**: + Start the built image and confirm it prints `BrokenOps multi-stage build`. + +5. **Verify the fix**: + Once the build and run both succeed, the lab is solved. diff --git a/labs/docker-multi-stage-copy-path/solution.sh b/labs/docker-multi-stage-copy-path/solution.sh new file mode 100755 index 0000000..17400b5 --- /dev/null +++ b/labs/docker-multi-stage-copy-path/solution.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +cd /opt/brokenops/docker-multi-stage-copy-path +sed -i 's/app.tx/app.txt/' Dockerfile diff --git a/labs/docker-multi-stage-copy-path/verify.sh b/labs/docker-multi-stage-copy-path/verify.sh new file mode 100755 index 0000000..490522a --- /dev/null +++ b/labs/docker-multi-stage-copy-path/verify.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail +cd /opt/brokenops/docker-multi-stage-copy-path +if ! docker build -t brokenops-multi-stage-copy . >/tmp/brokenops-multi-stage-build.log 2>&1; then + cat /tmp/brokenops-multi-stage-build.log + exit 1 +fi +if ! docker run --rm brokenops-multi-stage-copy sh -c 'test "$(cat /app/app.txt)" = "BrokenOps multi-stage build"'; then + docker run --rm brokenops-multi-stage-copy cat /app/app.txt || true + exit 1 +fi +echo "SUCCESS: The multi-stage build produces the expected artifact." +exit 0