From 4abeda6ce5f19ee24f76bc5ef45173d19211d40e Mon Sep 17 00:00:00 2001 From: "stefan.genie" Date: Fri, 12 Jun 2026 19:40:00 +0300 Subject: [PATCH 1/2] =?UTF-8?q?docs(lab1):=20add=20submission1=20=E2=80=94?= =?UTF-8?q?=20deploy=20and=20failure=20exploration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- submissions/lab1.md | 422 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 submissions/lab1.md diff --git a/submissions/lab1.md b/submissions/lab1.md new file mode 100644 index 0000000..0467b17 --- /dev/null +++ b/submissions/lab1.md @@ -0,0 +1,422 @@ +# Lab 1 Submission +## 1.1  +``` +$ docker compose ps +NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS +app-events-1 app-events "uvicorn main:app --…" events 30 minutes ago Up 5 minutes 0.0.0.0:8081->8081/tcp, [::]:8081->8081/tcp +app-gateway-1 app-gateway "uvicorn main:app --…" gateway 30 minutes ago Up 30 minutes 0.0.0.0:3080->8080/tcp, [::]:3080->8080/tcp +app-payments-1 app-payments "uvicorn main:app --…" payments 30 minutes ago Up 5 minutes 0.0.0.0:8082->8082/tcp, [::]:8082->8082/tcp +app-postgres-1 postgres:17-alpine "docker-entrypoint.s…" postgres 30 minutes ago Up 5 minutes (healthy) 0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp +app-redis-1 redis:7-alpine "docker-entrypoint.s…" redis 30 minutes ago Up 5 minutes (healthy) 0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp +``` +## 1.2 +```bash +$ curl -s http://localhost:3080/events | python3 -m json.tool +[ + { + "id": 1, + "name": "Go Conference 2026", + "venue": "Main Hall A", + "date": "2026-09-15T09:00:00+00:00", + "total_tickets": 100, + "price_cents": 5000, + "available": 100 + }, + { + "id": 4, + "name": "Python Workshop", + "venue": "Lab 301", + "date": "2026-09-22T14:00:00+00:00", + "total_tickets": 25, + "price_cents": 2000, + "available": 25 + }, + { + "id": 2, + "name": "SRE Meetup", + "venue": "Room 204", + "date": "2026-10-01T18:00:00+00:00", + "total_tickets": 30, + "price_cents": 0, + "available": 30 + }, + { + "id": 5, + "name": "Kubernetes Deep Dive", + "venue": "Auditorium B", + "date": "2026-10-10T10:00:00+00:00", + "total_tickets": 80, + "price_cents": 8000, + "available": 80 + }, + { + "id": 3, + "name": "Cloud Native Summit", + "venue": "Expo Center", + "date": "2026-11-20T10:00:00+00:00", + "total_tickets": 500, + "price_cents": 15000, + "available": 500 + } +] +``` + +```bash +$ curl -s -X POST http://localhost:3080/events/1/reserve \ + -H "Content-Type: application/json" \ + -d '{"quantity": 1}' | python3 -m json.tool +{ + "reservation_id": "daadfa05-8837-4f5c-91aa-2d14015b286f", + "event_id": 1, + "quantity": 1, + "total_cents": 5000, + "expires_in_seconds": 300 +} +``` +    +```shell +$ curl -s -X POST http://localhost:3080/reserve/RESERVATION_ID_HERE/pay | python3 -m json.tool +{ + "detail": "Payment service unavailable" +} +``` +   +```shell +$ curl -s http://localhost:3080/health | python3 -m json.tool +{ + "status": "degraded", + "checks": { + "events": "ok", + "payments": "down", + "circuit_payments": "CLOSED" + } +} +``` + +## 1.3 Dependency Map +### Service Call Graph +``` +gateway → events + +gateway → payments + +gateway → notifications (optional, fire-and-forget) + +events → postgres + +events → redis +``` + + +## 1.4: Systematic Failure Exploration + +For each scenario, one component was stopped with `docker compose stop `, the endpoints were tested through the gateway (`localhost:3080`), then the service was brought back with `docker compose start `. + +--- + +### Payments stopped + +```bash +$ docker compose stop payments + Container app-payments-1 Stopping + Container app-payments-1 Stopped +``` + +```bash +$ curl -s http://localhost:3080/events | python3 -m json.tool +[ + { + "id": 1, + "name": "Go Conference 2026", + "venue": "Main Hall A", + "date": "2026-09-15T09:00:00+00:00", + "total_tickets": 100, + "price_cents": 5000, + "available": 100 + }, + { + "id": 4, + "name": "Python Workshop", + "venue": "Lab 301", + "date": "2026-09-22T14:00:00+00:00", + "total_tickets": 25, + "price_cents": 2000, + "available": 25 + }, + { + "id": 2, + "name": "SRE Meetup", + "venue": "Room 204", + "date": "2026-10-01T18:00:00+00:00", + "total_tickets": 30, + "price_cents": 0, + "available": 30 + }, + { + "id": 5, + "name": "Kubernetes Deep Dive", + "venue": "Auditorium B", + "date": "2026-10-10T10:00:00+00:00", + "total_tickets": 80, + "price_cents": 8000, + "available": 80 + }, + { + "id": 3, + "name": "Cloud Native Summit", + "venue": "Expo Center", + "date": "2026-11-20T10:00:00+00:00", + "total_tickets": 500, + "price_cents": 15000, + "available": 500 + } +] +``` + +```bash +$ curl -s -X POST http://localhost:3080/events/1/reserve \ + -H "Content-Type: application/json" -d '{"quantity": 1}' +{"reservation_id":"ff8a3017-1743-4a58-b5e1-33c56e5b5b57","event_id":1,"quantity":1,"total_cents":5000,"expires_in_seconds":300} +``` + +```bash +$ curl -s -X POST http://localhost:3080/reserve/ff8a3017-1743-4a58-b5e1-33c56e5b5b57/pay +{"detail":"Payment service unavailable"} +``` + +```bash +$ curl -s http://localhost:3080/health | python3 -m json.tool +{ + "status": "degraded", + "checks": { + "events": "ok", + "payments": "down", + "circuit_payments": "CLOSED" + } +} +``` +(HTTP status: 503) + +```bash +$ docker compose start payments + Container app-payments-1 Starting + Container app-payments-1 Started +``` + +1. `GET /events` and `POST /events/{id}/reserve` — both go through the events service only. +2. `POST /reserve/{id}/pay` fails because the gateway cannot reach payments. +3. HTTP 502 with `{"detail":"Payment service unavailable"}`. +4. Yes. Returns HTTP 503, `"status": "degraded"`, and `"payments": "down"` while events stays `"ok"`. + +--- + +### Events stopped + +```bash +$ docker compose stop events + Container app-events-1 Stopping + Container app-events-1 Stopped +``` + +```bash +$ curl -s http://localhost:3080/events | python3 -m json.tool +{ + "detail": "Events service unavailable" +} +``` + +```bash +$ curl -s -X POST http://localhost:3080/events/1/reserve \ + -H "Content-Type: application/json" -d '{"quantity": 1}' +{"detail":"Events service unavailable"} +``` + +```bash +$ curl -s -X POST http://localhost:3080/reserve/test-id/pay +{"detail":"Payment succeeded but confirmation failed — contact support"} +``` + +```bash +$ curl -s http://localhost:3080/health | python3 -m json.tool +{ + "status": "degraded", + "checks": { + "events": "down", + "payments": "ok", + "circuit_payments": "CLOSED" + } +} +``` +(HTTP status: 503) + +```bash +$ docker compose start events + Container app-postgres-1 Waiting + Container app-redis-1 Waiting + Container app-redis-1 Healthy + Container app-postgres-1 Healthy + Container app-events-1 Starting + Container app-events-1 Started +``` + +1. None of the user-facing ticket flows work reliably. Payments is up, but listing and reserving both fail. Pay may charge the user (payments does not validate the reservation) and then fail at the confirmation step. +2. `GET /events`, `POST /events/{id}/reserve`, and the confirmation half of `POST /reserve/{id}/pay`. +3. HTTP 502 `{"detail":"Events service unavailable"}` for list/reserve; HTTP 500 `{"detail":"Payment succeeded but confirmation failed — contact support"}` if pay is attempted (dangerous partial failure). +4. Yes. HTTP 503, `"status": "degraded"`, `"events": "down"`. + +--- + +### Redis stopped + +```bash +$ docker compose stop redis + Container app-redis-1 Stopping + Container app-redis-1 Stopped +``` + +```bash +$ curl -s http://localhost:3080/events | python3 -m json.tool +[ + { + "id": 1, + "name": "Go Conference 2026", + "venue": "Main Hall A", + "date": "2026-09-15T09:00:00+00:00", + "total_tickets": 100, + "price_cents": 5000, + "available": 100 + }, + ... +] +``` + +```bash +$ curl -s -X POST http://localhost:3080/events/1/reserve \ + -H "Content-Type: application/json" -d '{"quantity": 1}' +{"detail":"Events service timeout"} +``` + +```bash +$ curl -s -X POST http://localhost:3080/reserve/test-id/pay +{"detail":"Payment succeeded but confirmation failed — contact support"} +``` + +```bash +$ curl -s http://localhost:3080/health | python3 -m json.tool +{ + "status": "degraded", + "checks": { + "events": "down", + "payments": "ok", + "circuit_payments": "CLOSED" + } +} +``` +(HTTP status: 503) + +```bash +$ docker compose start redis + Container app-redis-1 Starting + Container app-redis-1 Started +``` + +1. `GET /events` still works — event data is read from Postgres. Payments is also reachable. +2. `POST /events/{id}/reserve` times out (Redis is needed to hold reservations). Pay fails at confirmation because no reservation exists in Redis. +3. HTTP 504 `{"detail":"Events service timeout"}` on reserve; HTTP 500 on pay if attempted after a failed/missing reservation. +4. Yes, indirectly. Gateway reports `"events": "down"` (events `/health` returns 503 because Redis is down) even though read-only listing still works. Overall status is `"degraded"` with HTTP 503. + +--- + +### Postgres stopped + +```bash +$ docker compose stop postgres + Container app-postgres-1 Stopping + Container app-postgres-1 Stopped +``` + +```bash +$ curl -s http://localhost:3080/events | python3 -m json.tool +{ + "detail": "Events service unavailable" +} +``` + +```bash +$ curl -s -X POST http://localhost:3080/events/1/reserve \ + -H "Content-Type: application/json" -d '{"quantity": 1}' +Internal Server Error +``` + +```bash +$ curl -s -X POST http://localhost:3080/reserve/test-id/pay +{"detail":"Payment succeeded but confirmation failed — contact support"} +``` + +```bash +$ curl -s http://localhost:3080/health | python3 -m json.tool +{ + "status": "degraded", + "checks": { + "events": "degraded", + "payments": "ok", + "circuit_payments": "CLOSED" + } +} +``` +(HTTP status: 503) + +```bash +$ docker compose start postgres + Container app-postgres-1 Starting + Container app-postgres-1 Started +``` + +1. Only payments health/charge path is reachable. All event data operations break because Postgres stores events and orders. +2. `GET /events`, `POST /events/{id}/reserve`, and pay confirmation all fail. +3. HTTP 502 `{"detail":"Events service unavailable"}` for listing; HTTP 500 plain `Internal Server Error` on reserve (DB pool error); HTTP 500 on pay confirmation. +4. Yes. HTTP 503, `"status": "degraded"`, `"events": "degraded"` (events `/health` returns non-200 because Postgres check fails). +## Failure Table + +| Component Killed | Events List | Reserve | Pay | Health Check | User Impact | +| ---------------- | ------------------------------------------ | ------------------------------------------ | --------------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------- | +| payments | Works | Works | Fails (502 — `Payment service unavailable`) | Degraded (503) — `payments: down`, `events: ok` | Can browse events and reserve tickets; cannot complete payment | +| events | Fails (502 — `Events service unavailable`) | Fails (502 — `Events service unavailable`) | Fails (500 — payment may charge, confirmation fails) | Degraded (503) — `events: down`, `payments: ok` | Entire ticket flow broken; risky partial failure if user attempts pay | +| redis | Works | Fails (504 — `Events service timeout`) | Fails (500 — `Payment succeeded but confirmation failed`) | Degraded (503) — `events: down` (Redis check fails in events health) | Can browse events; cannot hold or complete a reservation | +| postgres | Fails (502 — `Events service unavailable`) | Fails (500 — `Internal Server Error`) | Fails (500 — confirmation fails after charge) | Degraded (503) — `events: degraded` (Postgres check fails) | Complete data-layer outage; no reliable event or order operations | + +## 1.5 Payload +```bash +[nix-shell:~/tmp/SRE-Intro/app]$ ./loadgen/run.sh 5 30 +QuickTicket Load Generator Target: http://localhost:3080 | RPS: 5 | Duration: 30s --- [10s] +requests=41 success=41 fail=0 error_rate=0% [10s] +requests=42 success=42 fail=0 error_rate=0% [10s] +requests=43 success=43 fail=0 error_rate=0% [10s] +requests=44 success=44 fail=0 error_rate=0% [20s] +requests=83 success=83 fail=0 error_rate=0% [20s] +requests=84 success=84 fail=0 error_rate=0% [20s] +requests=85 success=85 fail=0 error_rate=0% [20s] +requests=86 success=86 fail=0 error_rate=0% +--- +Done. total=124 success=124 fail=0 error_rate=0% +``` + +- Stopping payments after ~25s +```bash +[nix-shell:~/tmp/SRE-Intro/app]$ ./loadgen/run.sh 5 30 +QuickTicket Load Generator +Target: http://localhost:3080 | RPS: 5 | Duration: 30s +--- +[10s] requests=42 success=42 fail=0 error_rate=0% +[10s] requests=43 success=43 fail=0 error_rate=0% +[10s] requests=44 success=44 fail=0 error_rate=0% +[10s] requests=45 success=45 fail=0 error_rate=0% +[10s] requests=46 success=46 fail=0 error_rate=0% +[20s] requests=84 success=84 fail=0 error_rate=0% +[20s] requests=85 success=85 fail=0 error_rate=0% +[20s] requests=86 success=86 fail=0 error_rate=0% +[20s] requests=87 success=87 fail=0 error_rate=0% +--- +Done. total=125 success=124 fail=1 error_rate=.8% +``` + From cdcd41bac78e01ed73e741624cb2e2d11e91fff3 Mon Sep 17 00:00:00 2001 From: "stefan.genie" Date: Fri, 12 Jun 2026 19:51:28 +0300 Subject: [PATCH 2/2] =?UTF-8?q?docs(lab2):=20add=20lab2=20=E2=80=94=20Dock?= =?UTF-8?q?er=20inspection=20and=20optimization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- submissions/lab2.md | 311 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 submissions/lab2.md diff --git a/submissions/lab2.md b/submissions/lab2.md new file mode 100644 index 0000000..c42bad3 --- /dev/null +++ b/submissions/lab2.md @@ -0,0 +1,311 @@ +# Lab 2 Submission — Containerization: Inspect, Understand, Optimize + +## Task 1 — Docker Inspection & Operations + +### 2.1: Image inspection + +``` +$ docker images | grep app +app-events:latest 2ce41e398b95 231MB 57.1MB +app-gateway:latest 906662665e6a 211MB 52.1MB +app-payments:latest 452866262077 209MB 51.6MB +``` + +`app-events` is the largest image (231 MB) because it includes `psycopg2-binary` and `redis` in addition to FastAPI/Uvicorn. + +``` +$ docker history app-gateway --no-trunc --format "table {{.CreatedBy}}\t{{.Size}}" +CREATED BY SIZE +CMD ["uvicorn" "main:app" "--host" "0.0.0.0" "--port" "8080"] 0B +EXPOSE [8080/tcp] 0B +COPY main.py . # buildkit 16.4kB +RUN /bin/sh -c pip install --no-cache-dir -r requirements.txt # buildkit 28.1MB ← pip install layer +COPY requirements.txt . # buildkit 4.1kB +WORKDIR /app 0B +CMD ["python3"] 0B +RUN /bin/sh -c set -eux; for src in idle3 pip3 ... 20.5kB +RUN /bin/sh -c set -eux; savedAptMark="$(apt-mark showmanual)"; apt-get update; ... 39.2MB +ENV PYTHON_SHA256=... 0B +ENV PYTHON_VERSION=3.13.14 0B +ENV GPG_KEY=... 0B +RUN /bin/sh -c set -eux; apt-get update; apt-get install -y ... ca-certificates ... 5.57MB +ENV PATH=/usr/local/bin:... 0B +# debian.sh --arch 'amd64' out/ 'trixie' '@1781049600' 86.4MB +``` + +**How many layers?** 14 layers (counting each row in `docker history`). + +**Which layer is largest and why?** The Debian base image layer (`# debian.sh`) at **86.4 MB** is the largest — it is the full `python:3.13-slim` OS foundation. The next largest is the compiled Python runtime layer at **39.2 MB**. The **`pip install` layer is 28.1 MB** (third largest) — all Python dependencies (FastAPI, httpx, uvicorn, prometheus-client) land there. + +--- + +### 2.2: Container inspection + +``` +$ docker inspect app-events-1 --format '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +/app-events-1 172.18.0.5 + +$ docker inspect app-gateway-1 --format '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +/app-gateway-1 172.18.0.6 + +$ docker inspect app-payments-1 --format '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +/app-payments-1 172.18.0.2 +``` + +``` +$ docker inspect app-payments-1 --format '{{range .Config.Env}}{{println .}}{{end}}' +PAYMENT_LATENCY_MS=0 +PAYMENT_FAILURE_RATE=0.0 +PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305 +PYTHON_VERSION=3.13.14 +PYTHON_SHA256=639e43243c620a308f968213df9e00f2f8f62332f7adbaa7a7eeb9783057c690 +``` + +--- + +### 2.3: Live debugging with exec + +``` +$ docker exec app-gateway-1 whoami +root +``` + +``` +$ docker exec app-gateway-1 id +uid=0(root) gid=0(root) groups=0(root) +``` + +``` +$ docker exec app-gateway-1 cat /etc/resolv.conf +# Generated by Docker Engine. +nameserver 127.0.0.11 +options edns0 ndots:0 +``` + +``` +$ docker exec app-gateway-1 python3 -c " +import urllib.request +print(urllib.request.urlopen('http://events:8081/health').read().decode()) +" +{"status":"healthy","checks":{"postgres":"ok","redis":"ok"}} +``` + +``` +$ docker exec app-gateway-1 python3 -c " +import urllib.request +print(urllib.request.urlopen('http://payments:8082/health').read().decode()) +" +{"status":"healthy","failure_rate":0.0,"latency_ms":0} +``` + +``` +$ docker exec app-gateway-1 python3 -c "import socket; print(socket.gethostbyname('events'))" +172.18.0.5 +``` + +--- + +### 2.4: Logs analysis + +``` +$ docker compose logs gateway --tail=20 +gateway-1 | {"time":"2026-06-12 16:31:47,731","level":"INFO","service":"gateway","msg":"HTTP Request: GET http://events:8081/events \"HTTP/1.1 200 OK\""} +gateway-1 | INFO: 172.18.0.1:38468 - "GET /events HTTP/1.1" 200 OK +... +gateway-1 | {"time":"2026-06-12 16:44:39,811","level":"INFO","service":"gateway","msg":"HTTP Request: GET http://events:8081/events \"HTTP/1.1 200 OK\""} +gateway-1 | INFO: 172.18.0.1:46986 - "GET /events HTTP/1.1" 200 OK +gateway-1 | {"time":"2026-06-12 16:44:39,830","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://events:8081/events/1/reserve \"HTTP/1.1 200 OK\""} +gateway-1 | INFO: 172.18.0.1:46988 - "POST /events/1/reserve HTTP/1.1" 200 OK +``` + +``` +$ docker compose logs events --tail=20 +events-1 | INFO: 172.18.0.6:38528 - "GET /events HTTP/1.1" 200 OK +events-1 | {"time":"2026-06-12 16:44:39,829","level":"INFO","service":"events","msg":"Reserved 1 tickets for event 1: 3816da55-eb49-49df-a68b-78178a7d0c21"} +events-1 | INFO: 172.18.0.6:38528 - "POST /events/1/reserve HTTP/1.1" 200 OK +``` + +After generating traffic (`curl /events` + `curl POST /events/1/reserve`): + +``` +$ docker compose logs gateway --tail=5 +gateway-1 | {"time":"2026-06-12 16:44:39,811","level":"INFO","service":"gateway","msg":"HTTP Request: GET http://events:8081/events \"HTTP/1.1 200 OK\""} +gateway-1 | INFO: 172.18.0.1:46986 - "GET /events HTTP/1.1" 200 OK +gateway-1 | {"time":"2026-06-12 16:44:39,830","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://events:8081/events/1/reserve \"HTTP/1.1 200 OK\""} +gateway-1 | INFO: 172.18.0.1:46988 - "POST /events/1/reserve HTTP/1.1" 200 OK +``` + +``` +$ docker compose logs events --tail=5 +events-1 | INFO: 172.18.0.6:38528 - "GET /events HTTP/1.1" 200 OK +events-1 | {"time":"2026-06-12 16:44:39,829","level":"INFO","service":"events","msg":"Reserved 1 tickets for event 1: 3816da55-eb49-49df-a68b-78178a7d0c21"} +events-1 | INFO: 172.18.0.6:38528 - "POST /events/1/reserve HTTP/1.1" 200 OK +``` + +**Can you follow a single request across services?** Yes. The reserve request at `16:44:39,830` in the gateway log matches the events log at `16:44:39,829` (same second, gateway is ~1 ms later). The gateway source IP `172.18.0.6` on the events side is the gateway container's IP on the Docker network. + +--- + +### 2.5: Network inspection + +``` +$ docker network ls | grep app +ecf358f998fb app_default bridge local +``` + +``` +$ docker network inspect app_default --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}' +app-gateway-1: 172.18.0.6/16 +app-payments-1: 172.18.0.2/16 +app-events-1: 172.18.0.5/16 +app-redis-1: 172.18.0.3/16 +app-postgres-1: 172.18.0.4/16 +``` + +--- + +### 2.6: DNS service discovery answer + +**How does the gateway find the events service? What IP does `events` resolve to?** + +Docker Compose creates a bridge network (`app_default`) and registers each service name as a DNS entry on Docker's embedded DNS server at **`127.0.0.11`** (visible in `/etc/resolv.conf` inside every container). The gateway does not use a hardcoded IP — it calls `http://events:8081` (from the `EVENTS_URL` env var). Docker DNS resolves the hostname `events` to the events container's current IP on the network, which was **`172.18.0.5`** during testing. If a container is recreated, the IP may change, but the service name stays stable. + +--- + +## Task 2 — Dockerfile Optimization + +### Image sizes before and after + +**Before** (before `.dockerignore` and non-root user): + +``` +app-events:latest 84fe7943aae3 231MB 57.1MB +app-gateway:latest 2a5814670196 211MB 52.1MB +app-payments:latest 0eb9550ebb4a 209MB 51.6MB +``` + +**After** (`docker compose build --no-cache` + `.dockerignore` + non-root user): + +``` +app-events:latest 2ce41e398b95 231MB 57.1MB +app-gateway:latest 906662665e6a 211MB 52.1MB +app-payments:latest 452866262077 209MB 51.6MB +``` + +**Any difference?** No meaningful size change. Each service build context contains only `main.py` and `requirements.txt` — there is no `.git/`, `__pycache__/`, or other excluded files in those directories, so `.dockerignore` has nothing to strip. The non-root user layer adds only a few KB. + +### `.dockerignore` content + +Created in `app/gateway/.dockerignore`, `app/events/.dockerignore`, and `app/payments/.dockerignore`: + +``` +__pycache__ +*.pyc +.git +.env +*.md +.vscode +``` + +### Non-root user verification + +``` +$ docker exec app-gateway-1 whoami +app + +$ docker exec app-events-1 whoami +app + +$ docker exec app-payments-1 whoami +app +``` + +### `git diff` of Dockerfile changes + +```diff +diff --git a/app/events/Dockerfile b/app/events/Dockerfile +index c45a68c..b6cb18d 100644 +--- a/app/events/Dockerfile ++++ b/app/events/Dockerfile +@@ -6,4 +6,6 @@ RUN pip install --no-cache-dir -r requirements.txt + COPY main.py . + + EXPOSE 8081 ++RUN addgroup --system app && adduser --system --ingroup app app ++USER app + CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8081"] +diff --git a/app/gateway/Dockerfile b/app/gateway/Dockerfile +index 68ef075..71c6891 100644 +--- a/app/gateway/Dockerfile ++++ b/app/gateway/Dockerfile +@@ -6,4 +6,6 @@ RUN pip install --no-cache-dir -r requirements.txt + COPY main.py . + + EXPOSE 8080 ++RUN addgroup --system app && adduser --system --ingroup app app ++USER app + CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"] +diff --git a/app/payments/Dockerfile b/app/payments/Dockerfile +index 7f9e7c1..8cf997d 100644 +--- a/app/payments/Dockerfile ++++ b/app/payments/Dockerfile +@@ -6,4 +6,6 @@ RUN pip install --no-cache-dir -r requirements.txt + COPY main.py . + + EXPOSE 8082 ++RUN addgroup --system app && adduser --system --ingroup app app ++USER app + CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8082"] +``` + +--- + +## Bonus Task — Trace a Request Across Services + +Fresh stack (`docker compose down && docker compose up -d`), then: + +``` +$ RES=$(curl -s -X POST http://localhost:3080/events/1/reserve \ + -H "Content-Type: application/json" -d '{"quantity":1}') +$ RES_ID=$(echo "$RES" | python3 -c "import sys,json; print(json.load(sys.stdin)['reservation_id'])") +$ curl -s -X POST "http://localhost:3080/reserve/$RES_ID/pay" +{"order_id":"23dae506-c4a4-4fb5-9c0e-678aed7694f3","event_id":1,"quantity":1,"total_cents":5000,"status":"confirmed"} +``` + +### Timestamped logs (reservation `23dae506-c4a4-4fb5-9c0e-678aed7694f3`) + +``` +events-1 | 2026-06-12T16:46:10.719442000Z {"time":"2026-06-12 16:46:10,718","level":"INFO","service":"events","msg":"Reserved 1 tickets for event 1: 23dae506-c4a4-4fb5-9c0e-678aed7694f3"} +events-1 | 2026-06-12T16:46:10.721064000Z INFO: 172.18.0.6:38956 - "POST /events/1/reserve HTTP/1.1" 200 OK +gateway-1 | 2026-06-12T16:46:10.723013000Z {"time":"2026-06-12 16:46:10,722","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://events:8081/events/1/reserve \"HTTP/1.1 200 OK\""} +gateway-1 | 2026-06-12T16:46:10.724849000Z INFO: 172.18.0.1:53172 - "POST /events/1/reserve HTTP/1.1" 200 OK + +payments-1 | 2026-06-12T16:46:10.801518000Z {"time":"2026-06-12 16:46:10,800","level":"INFO","service":"payments","msg":"Payment success: PAY-C9800B4E for 23dae506-c4a4-4fb5-9c0e-678aed7694f3"} +payments-1 | 2026-06-12T16:46:10.802090000Z INFO: 172.18.0.6:49976 - "POST /charge HTTP/1.1" 200 OK +gateway-1 | 2026-06-12T16:46:10.803619000Z {"time":"2026-06-12 16:46:10,803","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://payments:8082/charge \"HTTP/1.1 200 OK\""} + +events-1 | 2026-06-12T16:46:10.824218000Z {"time":"2026-06-12 16:46:10,823","level":"INFO","service":"events","msg":"Order confirmed: 23dae506-c4a4-4fb5-9c0e-678aed7694f3"} +events-1 | 2026-06-12T16:46:10.825168000Z INFO: 172.18.0.6:38956 - "POST /reservations/23dae506-c4a4-4fb5-9c0e-678aed7694f3/confirm HTTP/1.1" 200 OK +gateway-1 | 2026-06-12T16:46:10.826221000Z {"time":"2026-06-12 16:46:10,825","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://events:8081/reservations/23dae506-c4a4-4fb5-9c0e-678aed7694f3/confirm \"HTTP/1.1 200 OK\""} +gateway-1 | 2026-06-12T16:46:10.828019000Z INFO: 172.18.0.1:53182 - "POST /reserve/23dae506-c4a4-4fb5-9c0e-678aed7694f3/pay HTTP/1.1" 200 OK +``` + +### Annotations + +| Timestamp | Service | Action | Δ from previous hop | +|-----------|---------|--------|---------------------| +| 16:46:10.718 | **events** | Creates reservation in Redis | — | +| 16:46:10.722 | **gateway** | Receives events reserve response | ~4 ms | +| 16:46:10.724 | **gateway** | Returns reserve 200 to client | ~2 ms | +| 16:46:10.800 | **payments** | Charges reservation, returns `PAY-C9800B4E` | ~76 ms (client → gateway → payments) | +| 16:46:10.803 | **gateway** | Receives payments response | ~3 ms | +| 16:46:10.823 | **events** | Confirms order in Postgres, deletes Redis hold | ~20 ms | +| 16:46:10.825 | **gateway** | Receives confirm response | ~2 ms | +| 16:46:10.828 | **gateway** | Returns pay 200 to client | ~3 ms | + +### End-to-end timing + +- **Reserve** (`POST /events/1/reserve`): gateway received request → returned 200 in **~6 ms** (10.718 events processing → 10.724 gateway response). +- **Pay** (`POST /reserve/{id}/pay`): **~43 ms** measured by curl (`time_total=0.043396`). From logs, gateway finished the pay handler at 10.828; the payments charge completed at 10.800, and events confirmation at 10.823. The slowest hop is gateway → payments (~76 ms gap includes client/network time between the two curl calls). +- **Full purchase** (reserve + pay): approximately **~50 ms** of service processing time, plus whatever delay between the two curl commands.