diff --git a/app/events/.dockerignore b/app/events/.dockerignore new file mode 100644 index 0000000..b9f779d --- /dev/null +++ b/app/events/.dockerignore @@ -0,0 +1,6 @@ +__pycache__ +*.pyc +.git +.env +*.md +.vscode diff --git a/app/events/Dockerfile b/app/events/Dockerfile index c45a68c..57148b7 100644 --- a/app/events/Dockerfile +++ b/app/events/Dockerfile @@ -4,6 +4,8 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py . +RUN addgroup --system app && adduser --system --ingroup app app && chown -R app:app /app +USER app EXPOSE 8081 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8081"] diff --git a/app/gateway/.dockerignore b/app/gateway/.dockerignore new file mode 100644 index 0000000..b9f779d --- /dev/null +++ b/app/gateway/.dockerignore @@ -0,0 +1,6 @@ +__pycache__ +*.pyc +.git +.env +*.md +.vscode diff --git a/app/gateway/Dockerfile b/app/gateway/Dockerfile index 68ef075..d3d3544 100644 --- a/app/gateway/Dockerfile +++ b/app/gateway/Dockerfile @@ -4,6 +4,8 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py . +RUN addgroup --system app && adduser --system --ingroup app app && chown -R app:app /app +USER app EXPOSE 8080 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/app/payments/.dockerignore b/app/payments/.dockerignore new file mode 100644 index 0000000..b9f779d --- /dev/null +++ b/app/payments/.dockerignore @@ -0,0 +1,6 @@ +__pycache__ +*.pyc +.git +.env +*.md +.vscode diff --git a/app/payments/Dockerfile b/app/payments/Dockerfile index 7f9e7c1..0ae3b8a 100644 --- a/app/payments/Dockerfile +++ b/app/payments/Dockerfile @@ -4,6 +4,8 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py . +RUN addgroup --system app && adduser --system --ingroup app app && chown -R app:app /app +USER app EXPOSE 8082 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8082"] diff --git a/submissions/lab2.md b/submissions/lab2.md new file mode 100644 index 0000000..8c0cb30 --- /dev/null +++ b/submissions/lab2.md @@ -0,0 +1,222 @@ +# Lab 2 + +## Task 1 + +I checked the Docker images first. + +```bash +docker images | grep app +``` + +```text +app-events:latest af008b8239e4 232MB 56.9MB +app-gateway:latest fec898af8ff0 213MB 51.9MB +app-payments:latest 6dc7367db006 211MB 51.4MB +``` + +The biggest image was `app-events`. I also looked at `app-gateway` layer history. + +```bash +docker history app-gateway --no-trunc --format "table {{.CreatedBy}}\t{{.Size}}" +``` + +```text +CMD ["uvicorn" "main:app" "--host" "0.0.0.0" "--port" "8080"] 0B +EXPOSE [8080/tcp] 0B +COPY main.py . # buildkit 24.6kB +RUN /bin/sh -c pip install --no-cache-dir -r requirements.txt 28.9MB +COPY requirements.txt . # buildkit 12.3kB +WORKDIR /app 8.19kB +CMD ["python3"] 0B +RUN /bin/sh -c set -eux; for src in idle3 ... 16.4kB +RUN /bin/sh -c set -eux; savedAptMark=... 39.9MB +ENV PYTHON_SHA256=... 0B +ENV PYTHON_VERSION=3.13.12 0B +ENV GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305 0B +``` + +`app-gateway` has 15 layers. The biggest layer is the big Python base image build step at `39.9MB`. The biggest app-specific layer is `pip install` at `28.9MB`, because that is where the Python packages are added. + +Then I checked the containers on the Docker network. + +```bash +docker inspect app-events-1 --format '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +docker inspect app-gateway-1 --format '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +docker inspect app-payments-1 --format '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +``` + +```text +/app-events-1 172.28.0.5 +/app-gateway-1 172.28.0.6 +/app-payments-1 172.28.0.2 +``` + +```bash +docker inspect app-payments-1 --format '{{range .Config.Env}}{{println .}}{{end}}' +``` + +```text +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.12 +PYTHON_SHA256=2a84cd31dd8d8ea8aaff75de66fc1b4b0127dd5799aa50a64ae9a313885b4593 +``` + +After that I went inside the gateway container. + +```bash +docker exec app-gateway-1 whoami +docker exec app-gateway-1 id +docker exec app-gateway-1 cat /etc/resolv.conf +docker exec app-gateway-1 python3 -c "import urllib.request; print(urllib.request.urlopen('http://events:8081/health').read().decode())" +docker exec app-gateway-1 python3 -c "import urllib.request; print(urllib.request.urlopen('http://payments:8082/health').read().decode())" +``` + +```text +root +uid=0(root) gid=0(root) groups=0(root) + +# Generated by Docker Engine. +# This file can be edited; Docker Engine will not make further changes once it +# has been modified. + +nameserver 127.0.0.11 +options ndots:0 + +{"status":"healthy","checks":{"postgres":"ok","redis":"ok"}} +{"status":"healthy","failure_rate":0.0,"latency_ms":0} +``` + +So before the optimization part, the gateway container was running as `root`. DNS inside the container uses Docker's internal resolver at `127.0.0.11`. + +I also checked the service logs and made one request flow. + +```bash +curl -s http://localhost:3080/events > /dev/null +curl -s -X POST http://localhost:3080/events/1/reserve -H "Content-Type: application/json" --data-binary '{"quantity":1}' +docker compose logs gateway --tail=10 +docker compose logs events --tail=10 +``` + +```text +gateway-1 | {"time":"2026-06-12 20:38:13,112","level":"INFO","service":"gateway","msg":"HTTP Request: GET http://events:8081/events "HTTP/1.1 200 OK""} +gateway-1 | INFO: 172.28.0.1:48642 - "GET /events HTTP/1.1" 200 OK +gateway-1 | {"time":"2026-06-12 20:38:13,170","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://events:8081/events/1/reserve "HTTP/1.1 200 OK""} +gateway-1 | INFO: 172.28.0.1:48652 - "POST /events/1/reserve HTTP/1.1" 200 OK +events-1 | INFO: 172.28.0.6:47654 - "GET /events HTTP/1.1" 200 OK +events-1 | {"time":"2026-06-12 20:38:13,169","level":"INFO","service":"events","msg":"Reserved 1 tickets for event 1: 1f852b86-1a1b-4313-977b-3a4eb7573f76"} +events-1 | INFO: 172.28.0.6:47654 - "POST /events/1/reserve HTTP/1.1" 200 OK +``` + +This was enough to follow one request from `gateway` to `events` by timestamp. + +I also checked the Docker network itself. + +```bash +docker network ls | grep app +docker network inspect app_default --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}' +``` + +```text +73ca602c3a15 app_default bridge local + +app-postgres-1: 172.28.0.4/16 +app-gateway-1: 172.28.0.6/16 +app-redis-1: 172.28.0.3/16 +app-events-1: 172.28.0.5/16 +app-payments-1: 172.28.0.2/16 +``` + +The gateway finds the `events` service through Docker Compose DNS. Inside the container, the name `events` is resolved by `127.0.0.11`, and in this run it resolved to `172.28.0.5`. + +## Task 2 + +I added the same `.dockerignore` file to `app/gateway`, `app/events`, and `app/payments`. + +```text +__pycache__ +*.pyc +.git +.env +*.md +.vscode +``` + +Image sizes before rebuild: + +```text +app-events:latest af008b8239e4 232MB 56.9MB +app-gateway:latest fec898af8ff0 213MB 51.9MB +app-payments:latest 6dc7367db006 211MB 51.4MB +``` + +Image sizes after rebuild: + +```text +app-events:latest e189a1c71f13 233MB 56.9MB +app-gateway:latest 66cb7b15061b 213MB 51.9MB +app-payments:latest 287897942389 211MB 51.4MB +``` + +The size change was basically zero here. That makes sense because the build context was already very small. + +I also changed all three Dockerfiles to add a non-root user and switch to it. + +Files: `app/gateway/Dockerfile`, `app/events/Dockerfile`, `app/payments/Dockerfile` +Line: before `EXPOSE` and `CMD` + +```dockerfile +RUN addgroup --system app && adduser --system --ingroup app app && chown -R app:app /app +USER app +``` + +This creates a non-root user called `app`, gives it access to `/app`, and runs the container as that user instead of `root`. + +After rebuild, `whoami` inside the gateway container changed from `root` to `app`. + +```bash +docker exec app-gateway-1 whoami +``` + +```text +app +``` + +## Bonus Task + +I cleared the logs, ran one full purchase flow, and then captured `docker compose logs --timestamps`. + +Reserve output: + +```json +{"reservation_id":"6d2f601b-155a-4a52-96c3-2a108044aff4","event_id":1,"quantity":1,"total_cents":5000,"expires_in_seconds":300} +``` + +Pay output: + +```json +{"order_id":"6d2f601b-155a-4a52-96c3-2a108044aff4","event_id":1,"quantity":1,"total_cents":5000,"status":"confirmed"} +``` + +Timestamped log flow: + +```text +events-1 | 2026-06-12T20:40:56.378886950Z {"time":"2026-06-12 20:40:56,378","level":"INFO","service":"events","msg":"Reserved 1 tickets for event 1: 6d2f601b-155a-4a52-96c3-2a108044aff4"} +gateway-1 | 2026-06-12T20:40:56.380978684Z {"time":"2026-06-12 20:40:56,380","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://events:8081/events/1/reserve "HTTP/1.1 200 OK""} +gateway-1 | 2026-06-12T20:40:56.381938864Z INFO: 172.28.0.1:36392 - "POST /events/1/reserve HTTP/1.1" 200 OK +payments-1 | 2026-06-12T20:40:56.639541159Z {"time":"2026-06-12 20:40:56,639","level":"INFO","service":"payments","msg":"Payment success: PAY-00DC3351 for 6d2f601b-155a-4a52-96c3-2a108044aff4"} +gateway-1 | 2026-06-12T20:40:56.640944621Z {"time":"2026-06-12 20:40:56,640","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://payments:8082/charge "HTTP/1.1 200 OK""} +events-1 | 2026-06-12T20:40:56.646830345Z {"time":"2026-06-12 20:40:56,646","level":"INFO","service":"events","msg":"Order confirmed: 6d2f601b-155a-4a52-96c3-2a108044aff4"} +gateway-1 | 2026-06-12T20:40:56.647926470Z {"time":"2026-06-12 20:40:56,647","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://events:8081/reservations/6d2f601b-155a-4a52-96c3-2a108044aff4/confirm "HTTP/1.1 200 OK""} +gateway-1 | 2026-06-12T20:40:56.648985581Z INFO: 172.28.0.1:36396 - "POST /reserve/6d2f601b-155a-4a52-96c3-2a108044aff4/pay HTTP/1.1" 200 OK +``` + +- `20:40:56.378` - `events` - reservation created +- `20:40:56.380` - `gateway` - reserve call to `events` returned `200` +- `20:40:56.639` - `payments` - charge completed +- `20:40:56.646` - `events` - order confirmed +- `20:40:56.648` - `gateway` - final pay response returned `200` + +Total end-to-end time from the first reserve log to the final gateway pay response was about `270 ms`.