diff --git a/.gitignore b/.gitignore index bc2bd89..66ece8f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ # Student submission directory (students submit via Moodle/link, not git) # DO NOT write to this directory during labs — it's for manual grading/inspection only. -submissions/ +# submissions/ # Instructor-only reference submissions (dry-run results, not student deliverables) # These show what "correct" looks like; students don't see them. diff --git a/app/events/.dockerignore b/app/events/.dockerignore new file mode 100644 index 0000000..99ff1a6 --- /dev/null +++ b/app/events/.dockerignore @@ -0,0 +1,6 @@ +__pycache__ +*.pyc +.git +.env +*.md +.vscode \ No newline at end of file diff --git a/app/events/Dockerfile b/app/events/Dockerfile index c45a68c..1976d04 100644 --- a/app/events/Dockerfile +++ b/app/events/Dockerfile @@ -6,4 +6,7 @@ RUN pip install --no-cache-dir -r requirements.txt COPY main.py . EXPOSE 8081 +RUN addgroup --system app && adduser --system --ingroup app app +RUN chown -R app:app /app +USER app 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..99ff1a6 --- /dev/null +++ b/app/gateway/.dockerignore @@ -0,0 +1,6 @@ +__pycache__ +*.pyc +.git +.env +*.md +.vscode \ No newline at end of file diff --git a/app/gateway/Dockerfile b/app/gateway/Dockerfile index 68ef075..3a695dc 100644 --- a/app/gateway/Dockerfile +++ b/app/gateway/Dockerfile @@ -6,4 +6,7 @@ RUN pip install --no-cache-dir -r requirements.txt COPY main.py . EXPOSE 8080 +RUN addgroup --system app && adduser --system --ingroup app app +RUN chown -R app:app /app +USER app 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..99ff1a6 --- /dev/null +++ b/app/payments/.dockerignore @@ -0,0 +1,6 @@ +__pycache__ +*.pyc +.git +.env +*.md +.vscode \ No newline at end of file diff --git a/app/payments/Dockerfile b/app/payments/Dockerfile index 7f9e7c1..a2f732b 100644 --- a/app/payments/Dockerfile +++ b/app/payments/Dockerfile @@ -6,4 +6,7 @@ RUN pip install --no-cache-dir -r requirements.txt COPY main.py . EXPOSE 8082 +RUN addgroup --system app && adduser --system --ingroup app app +RUN chown -R app:app /app +USER app 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..2fd67d1 --- /dev/null +++ b/submissions/lab2.md @@ -0,0 +1,280 @@ +# Lab 2 — Containerization: Inspect, Understand, Optimize + +## Task 1 — Docker Inspection & Operations + +### 1. Image sizes + +```bash +docker images | grep app +``` +``` +WARNING: This output is designed for human readability. For machine-readable output, please use --format. +app-events:latest 6397f372fc77 233MB 57.1MB U +app-gateway:latest 857a694d6341 214MB 52.1MB U +app-payments:latest d1ce7c7b77b7 212MB 51.6MB U +``` + +### 2. Layer history (gateway) + +```bash +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 24.6kB +RUN /bin/sh -c pip install --no-cache-dir -r requirements.txt # buildkit 29.2MB +COPY requirements.txt . # buildkit 12.3kB +WORKDIR /app 8.19kB +CMD ["python3"] 0B +RUN /bin/sh -c set -eux; for src in idle3 pip3 pydoc3 python3 python3-config; do dst="$(echo "$src" | tr -d 3)"; [ -s "/usr/local/bin/$src" ]; [ ! -e "/usr/local/bin/$dst" ]; ln -svT "$src" "/usr/local/bin/$dst"; done # buildkit 16.4kB +RUN /bin/sh -c set -eux; savedAptMark="$(apt-mark showmanual)"; apt-get update; apt-get install -y --no-install-recommends dpkg-dev gcc gnupg libbluetooth-dev libbz2-dev libc6-dev libdb-dev libffi-dev libgdbm-dev liblzma-dev libncursesw5-dev libreadline-dev libsqlite3-dev libssl-dev make tk-dev uuid-dev wget xz-utils zlib1g-dev ; wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz"; echo "$PYTHON_SHA256 *python.tar.xz" | sha256sum -c -; wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc"; GNUPGHOME="$(mktemp -d)"; export GNUPGHOME; gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$GPG_KEY"; gpg --batch --verify python.tar.xz.asc python.tar.xz; gpgconf --kill all; rm -rf "$GNUPGHOME" python.tar.xz.asc; mkdir -p /usr/src/python; tar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz; rm python.tar.xz; cd /usr/src/python; gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; ./configure --build="$gnuArch" --enable-loadable-sqlite-extensions --enable-optimizations --enable-option-checking=fatal --enable-shared $(test "${gnuArch%%-*}" != 'riscv64' && echo '--with-lto') --with-ensurepip ; nproc="$(nproc)"; EXTRA_CFLAGS="$(dpkg-buildflags --get CFLAGS)"; LDFLAGS="$(dpkg-buildflags --get LDFLAGS)"; LDFLAGS="${LDFLAGS:-} -Wl,--strip-all"; arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; case "$arch" in amd64|arm64) EXTRA_CFLAGS="${EXTRA_CFLAGS:-} -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer"; ;; i386) ;; *) EXTRA_CFLAGS="${EXTRA_CFLAGS:-} -fno-omit-frame-pointer"; ;; esac; make -j "$nproc" "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" "LDFLAGS=${LDFLAGS:-}" ; rm python; make -j "$nproc" "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" "LDFLAGS=${LDFLAGS:-} -Wl,-rpath='\$\$ORIGIN/../lib'" python ; make install; cd /; rm -rf /usr/src/python; find /usr/local -depth \( \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) -o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \) \) \) -exec rm -rf '{}' + ; ldconfig; apt-mark auto '.*' > /dev/null; apt-mark manual $savedAptMark; find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec ldd '{}' ';' | awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); printf "*%s\n", so }' | sort -u | xargs -rt dpkg-query --search | awk 'sub(":$", "", $1) { print $1 }' | sort -u | xargs -r apt-mark manual ; apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; apt-get dist-clean; export PYTHONDONTWRITEBYTECODE=1; python3 --version; pip3 --version # buildkit 40.2MB +ENV PYTHON_SHA256=639e43243c620a308f968213df9e00f2f8f62332f7adbaa7a7eeb9783057c690 0B +ENV PYTHON_VERSION=3.13.14 0B +ENV GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305 0B +RUN /bin/sh -c set -eux; apt-get update; apt-get install -y --no-install-recommends ca-certificates netbase tzdata ; apt-get dist-clean # buildkit 4.94MB +ENV PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 0B +# debian.sh --arch 'amd64' out/ 'trixie' '@1781049600' 87.4MB +``` + +**Annotation: Largest layer overall:** the base image layer # debian.sh --arch 'amd64' ... at 87.4MB — this is the +Debian root filesystem itself, inherited from python:3.13-slim, not something your Dockerfile controls directly. + +**Largest layer added by Dockerfile:** RUN pip install --no-cache-dir -r requirements.txt at 29.2MB — this +installs fastapi, uvicorn, httpx, prometheus-client and all their transitive dependencies (starlette, pydantic, anyio, h11, etc.), which together dwarf the actual application code (main.py is only 24.6kB). + +### 3. Container IPs + +```bash +docker inspect app-events-1 --format '...' +docker inspect app-gateway-1 --format '...' +docker inspect app-payments-1 --format '...' +``` +``` +/app-events-1 172.18.0.5 +/app-gateway-1 172.18.0.6 +/app-payments-1 172.18.0.2 +``` + +### 4. Payments environment variables + +```bash +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 +``` + + +### 5. whoami + service discovery from inside gateway + +```bash +docker exec app-gateway-1 whoami +``` +``` +root +``` +```bash +docker exec app-gateway-1 id +``` +``` +uid=0(root) gid=0(root) groups=0(root) +``` +```bash +docker exec app-gateway-1 cat /etc/resolv.conf +``` +``` +# 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 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# ExtServers: [host(10.255.255.254)] +# Overrides: [] +# Option ndots from: internal +``` +```bash +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"}} +``` +```bash +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} +``` + +### 6. Log snippet — request through gateway → events + +```bash +curl -s http://localhost:3080/events > /dev/null +curl -s -X POST http://localhost:3080/events/1/reserve -H "Content-Type: application/json" -d '{"quantity":1}' +docker compose logs gateway --tail=5 +docker compose logs events --tail=5 +``` +``` +{"reservation_id":"d262b824-ef98-4a07-be33-7f222503544b","event_id":1,"quantity":1,"total_cents":5000,"expires_in_seconds":300} +gateway-1 | INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) +gateway-1 | {"time":"2026-06-12 11:12:01,828","level":"INFO","service":"gateway","msg":"HTTP Request: GET http://events:8081/events "HTTP/1.1 200 OK""} +gateway-1 | INFO: 172.18.0.1:47756 - "GET /events HTTP/1.1" 200 OK +gateway-1 | {"time":"2026-06-12 11:12:07,691","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:47764 - "POST /events/1/reserve HTTP/1.1" 200 OK +events-1 | INFO: Uvicorn running on http://0.0.0.0:8081 (Press CTRL+C to quit) +events-1 | INFO: 172.18.0.6:59684 - "GET /health HTTP/1.1" 200 OK +events-1 | INFO: 172.18.0.6:43460 - "GET /events HTTP/1.1" 200 OK +events-1 | {"time":"2026-06-12 11:12:07,688","level":"INFO","service":"events","msg":"Reserved 1 tickets for event 1: d262b824-ef98-4a07-be33-7f222503544b"} +events-1 | INFO: 172.18.0.6:43462 - "POST /events/1/reserve HTTP/1.1" 200 OK +``` + +### 7. Network inspect + +```bash +docker network ls | grep app +docker network inspect app_default --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}' +``` +``` +b780aa5c9854 app_default bridge local +app-redis-1: 172.18.0.3/16 +app-events-1: 172.18.0.5/16 +app-payments-1: 172.18.0.2/16 +app-gateway-1: 172.18.0.6/16 +app-postgres-1: 172.18.0.4/16 +``` + +### 8. How does the gateway find the events service? + +The gateway connects to `http://events:8081`, where `events` is the **service name** defined in `docker-compose.yml`. Docker Compose places all services on the same custom bridge network (`app_default`) and runs an embedded DNS server at `127.0.0.11` (visible in `/etc/resolv.conf` inside the containers). When the gateway resolves the hostname `events`, this DNS server returns the container's internal IP address — `172.18.0.5 ` — allowing service discovery without hardcoded IPs. + +--- + +## Task 2 — Dockerfile Optimization + +### .dockerignore content +``` +pycache +*.pyc +.git +.env +*.md +.vscode +``` + +### Image sizes before / after + +``` +# Before +app-events:latest 6397f372fc77 233MB 57.1MB U +app-gateway:latest 857a694d6341 214MB 52.1MB U +app-payments:latest d1ce7c7b77b7 212MB 51.6MB U +``` +``` +# After +app-events:latest f226bb61550f 233MB 57.1MB +app-gateway:latest 02871982821d 214MB 52.1MB +app-payments:latest 6e08cde5a073 212MB 51.6MB +``` + +**Observation:** Image sizes were unchanged (233MB/214MB/212MB before and after) despite different image IDs. This is expected — the build context for each service only contains main.py, requirements.txt, and Dockerfile; there's no .git, __pycache__, or .md files to exclude, so .dockerignore has no measurable effect on size in this case. It's still good practice for future-proofing the build context. + +### Non-root user verification + +```bash +docker exec app-gateway-1 whoami +``` +``` +app +``` + +### Dockerfile diff + +```diff +diff --git a/app/events/Dockerfile b/app/events/Dockerfile +index c45a68c..1976d04 100644 +--- a/app/events/Dockerfile ++++ b/app/events/Dockerfile +@@ -6,4 +6,7 @@ RUN pip install --no-cache-dir -r requirements.txt + COPY main.py . + + EXPOSE 8081 ++RUN addgroup --system app && adduser --system --ingroup app app ++RUN chown -R app:app /app ++USER app + CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8081"] +``` +```diff +diff --git a/app/gateway/Dockerfile b/app/gateway/Dockerfile +index 68ef075..3a695dc 100644 +--- a/app/gateway/Dockerfile ++++ b/app/gateway/Dockerfile +@@ -6,4 +6,7 @@ RUN pip install --no-cache-dir -r requirements.txt + COPY main.py . + + EXPOSE 8080 ++RUN addgroup --system app && adduser --system --ingroup app app ++RUN chown -R app:app /app ++USER app + CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"] +``` +```diff +diff --git a/app/payments/Dockerfile b/app/payments/Dockerfile +index 7f9e7c1..a2f732b 100644 +--- a/app/payments/Dockerfile ++++ b/app/payments/Dockerfile +@@ -6,4 +6,7 @@ RUN pip install --no-cache-dir -r requirements.txt + COPY main.py . + + EXPOSE 8082 ++RUN addgroup --system app && adduser --system --ingroup app app ++RUN chown -R app:app /app ++USER app + CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8082"] +``` + +--- + +## Bonus Task — Request Tracing + +### Full timestamped logs +``` +events-1 | 2026-06-12T11:21:29.729205458Z {"time":"2026-06-12 11:21:29,728","level":"INFO","service":"events","msg":"Reserved 1 tickets for event 1: 8fa5fedc-9ce5-4acc-9054-f27bda2d87f7"} +gateway-1 | 2026-06-12T11:21:29.733535074Z {"time":"2026-06-12 11:21:29,732","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://events:8081/events/1/reserve \"HTTP/1.1 200 OK\""} +gateway-1 | 2026-06-12T11:21:29.736102545Z INFO: 172.18.0.1:51232 - "POST /events/1/reserve HTTP/1.1" 200 OK + +payments-1 | 2026-06-12T11:21:36.829528156Z {"time":"2026-06-12 11:21:36,828","level":"INFO","service":"payments","msg":"Payment success: PAY-43EA99F3 for 8fa5fedc-9ce5-4acc-9054-f27bda2d87f7"} +payments-1 | 2026-06-12T11:21:36.830555075Z INFO: 172.18.0.6:47570 - "POST /charge HTTP/1.1" 200 OK +gateway-1 | 2026-06-12T11:21:36.833010992Z {"time":"2026-06-12 11:21:36,832","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://payments:8082/charge \"HTTP/1.1 200 OK\""} +events-1 | 2026-06-12T11:21:36.866966768Z {"time":"2026-06-12 11:21:36,866","level":"INFO","service":"events","msg":"Order confirmed: 8fa5fedc-9ce5-4acc-9054-f27bda2d87f7"} +events-1 | 2026-06-12T11:21:36.868226201Z INFO: 172.18.0.6:53692 - "POST /reservations/8fa5fedc-9ce5-4acc-9054-f27bda2d87f7/confirm HTTP/1.1" 200 OK +gateway-1 | 2026-06-12T11:21:36.869624671Z {"time":"2026-06-12 11:21:36,868","level":"INFO","service":"gateway","msg":"HTTP Request: POST http://events:8081/reservations/8fa5fedc-9ce5-4acc-9054-f27bda2d87f7/confirm \"HTTP/1.1 200 OK\""} +gateway-1 | 2026-06-12T11:21:36.871677897Z INFO: 172.18.0.1:51238 - "POST /reserve/8fa5fedc-9ce5-4acc-9054-f27bda2d87f7/pay HTTP/1.1" 200 OK +``` +### Annotated trace + +| Timestamp | Service | Action | Δ from previous | +|--|---|---|---------| +| 11:21:36.829528 | payments | Processed /charge, returned payment_ref | — | +| 11:21:36.830555 | payments | Responds 200 OK to gateway | +1.0ms | +| 11:21:36.833011 | gateway | Received response from payments | +2.5ms | +| 11:21:36.866967 | events | Confirmed order via /reservations/{id}/confirm | +33.9ms | +| 11:21:36.868226 | events | Responds 200 OK to gateway | +1.3ms | +| 11:21:36.869625 | gateway | Received confirmation from events | +1.4ms | +| 11:21:36.871678 | gateway | Returned final 200 OK response to client | +2.1ms | + +### End-to-end time + +Total time for the `/pay` request, measured from the payments service completing the charge (`11:21:36.829528156`) to the gateway returning its final response (`11:21:36.871677897`): + +**~42.1ms** + +The largest single hop is the events confirm step (~33.9ms between the gateway receiving the payment confirmation and the events service logging "Order confirmed") — this is the dominant cost in the request, likely dominated by the Postgres `INSERT INTO orders` plus the Redis cleanup (`DELETE reservation`) inside `confirm_reservation`. \ No newline at end of file