Skip to main content
>_ supraj.dev

Module 6: Evaluation & Production Ops · 6.75h

01 · UNDERSTAND

Day 38 Theory — Docker and Production Packaging

Why packaging matters

A Python application that works on one developer laptop is not yet a deployable production artifact.

The machine may accidentally provide:

  • the right Python version,
  • globally installed libraries,
  • local configuration files,
  • writable directories,
  • credentials,
  • system packages that were never documented.

If production depends on those hidden assumptions, deployment becomes unpredictable.

A container image gives us a repeatable package containing the application filesystem and runtime metadata needed to start the process.

Docker does not make weak software reliable. It makes the software packaged in a repeatable form so the same artifact can move through environments.

Image versus container

These words are often mixed together.

An image is a packaged, immutable set of filesystem layers plus metadata.

A container is a running process created from an image with runtime configuration and isolation supplied by the container runtime/operating system.

Dockerfile
    |
    v
docker build
    |
    v
Image
    |
    v
docker run / orchestrator
    |
    v
Container process

Think of the image as the packaged artifact and the container as one execution of that artifact.

A container is still a process

Do not think of a container as a tiny virtual machine.

At runtime, your application is still a process managed by an operating system kernel. Container isolation commonly relies on operating-system mechanisms such as namespaces and resource controls.

This matters because normal process rules still apply:

  • signals matter,
  • exit codes matter,
  • file permissions matter,
  • memory limits matter,
  • network sockets matter.

Docker changes packaging and isolation; it does not eliminate operating-system behavior.

Dockerfile as a build recipe

A Dockerfile describes how to build the image.

A simplified Python service might look like:

FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "-m", "service"]

Each instruction should exist for a reason.

Ask:

  • Which base image do we trust?
  • Which files enter the image?
  • Which dependency versions are resolved?
  • Which user runs the process?
  • What command becomes PID 1 inside the container?

Build context and .dockerignore

When Docker builds an image, files in the build context may be available to COPY instructions.

Do not send unnecessary files into the build context.

A .dockerignore can exclude items such as:

.git
.venv
__pycache__
.env
local test output
large temporary artifacts

This improves build efficiency and reduces the chance of accidentally packaging sensitive or irrelevant data.

A .dockerignore is not a substitute for real secret management, but it is part of a disciplined build boundary.

Layers and cache

Image builds are organized into layers. Layer reuse can make rebuilds much faster.

Consider two patterns.

Weak cache pattern:

COPY . .
RUN pip install -r requirements.txt

Any source-code change can invalidate the earlier copy layer and force dependency installation again.

Better pattern:

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

Now changing ordinary application code does not necessarily invalidate the dependency-install layer.

The broader principle is:

Put relatively stable dependency inputs before frequently changing application files when that ordering is correct for your build.

Do not optimize cache at the expense of correctness.

Layer history and secrets

A critical beginner misconception is:

COPY .env /tmp/.env
RUN use-secret /tmp/.env
RUN rm /tmp/.env

and then assuming the secret is gone.

Image layers are content history. Deleting a file in a later layer does not necessarily erase it from earlier image data.

Do not bake credentials into the image in the first place.

Avoid:

  • copying .env,
  • embedding API keys in source,
  • writing secrets into ARG/commands where they can persist in build metadata or layers,
  • committing credentials to the build context.

Use approved build-secret mechanisms when a secret is genuinely required during a build, and inject application secrets securely at runtime.

Multi-stage builds

Sometimes build tools are needed to create an artifact but are unnecessary at runtime.

A multi-stage build separates those concerns.

Builder stage
  - compiler
  - development headers
  - build tooling
  - produce artifact
        |
        v
Runtime stage
  - minimal runtime
  - application artifact only

Benefits can include:

  • smaller image,
  • fewer packages,
  • reduced attack surface,
  • clearer distinction between build and runtime dependencies.

Do not use multi-stage builds merely because they look “production grade.” Use them when they simplify or harden the runtime artifact.

Run as a non-root user

Running the application as root inside a container gives the process unnecessary privilege inside its isolation boundary.

A safer image normally creates a dedicated runtime user and gives that user only the filesystem permissions the application needs.

Example idea:

RUN adduser --disabled-password --gecos "" appuser
USER appuser

Exact commands vary by base image.

At deployment time, platforms can add further controls such as:

  • read-only root filesystem,
  • dropped Linux capabilities,
  • allowPrivilegeEscalation: false,
  • seccomp profiles,
  • explicit UID/GID policy.

Container hardening works in layers.

The filesystem should match application needs

If the application only needs to read packaged code and write temporary data to /tmp, do not assume the entire image filesystem must be writable.

Design explicit writable locations for:

  • temporary files,
  • caches if needed,
  • generated runtime artifacts.

Persistent business data should generally live in an appropriate external persistence service or mounted persistent storage—not in the container's ephemeral writable layer by accident.

Reproducible dependencies

A production build should not silently resolve unrelated future dependency versions every time it runs.

Use the dependency-locking strategy appropriate to the Python project.

The goal is:

same source + same declared dependency lock + same build inputs
          -> predictable application artifact

Perfect bit-for-bit reproducibility can require additional controls, but dependency pinning/locking is a foundational step.

Base images are supply-chain dependencies

FROM python:... is not just syntax. The base image becomes part of your software supply chain.

Consider:

  • trusted registry/source,
  • maintained version,
  • vulnerability posture,
  • digest pinning strategy where appropriate,
  • update/patch process.

A tiny image is not automatically safer if nobody maintains it.

Security is about known, maintained and minimized dependencies—not only image size.

Build once, promote the same artifact

A mature delivery flow avoids rebuilding different application images for test, staging and production from the same source if the intention is to deploy the exact tested artifact.

Conceptually:

commit
  -> CI tests
  -> build image sha256:abc...
  -> scan/verify
  -> staging uses sha256:abc...
  -> production promotes sha256:abc...

Environment-specific configuration should usually be supplied at runtime rather than creating a new application image for every environment.

This gives stronger evidence that production is running what was tested.

Health checks answer different questions

Orchestrators may check several aspects of application health.

Do not collapse them into one endpoint without understanding the semantics.

Conceptually:

  • startup — has initialization completed?
  • readiness — should this instance receive traffic now?
  • liveness — is the process stuck in a state where restart may be appropriate?

For example, temporary model-provider failure may make one feature degraded without necessarily meaning the application process is dead.

Bad health-check design can cause restart loops during external outages.

Signals and graceful shutdown

Containers are expected to stop.

Deployments roll, nodes terminate, autoscalers reduce capacity and operators restart services.

The process should handle termination signals deliberately.

A graceful shutdown sequence may look like:

SIGTERM
   |
   v
stop accepting new work
   |
   v
finish/cancel bounded in-flight work
   |
   v
close connections / flush telemetry
   |
   v
exit before termination deadline

A process that ignores shutdown behavior can drop requests or leave work in uncertain states.

Resource limits affect application behavior

A container can be given CPU and memory constraints.

The application should be tested within realistic limits.

An AI API client that creates unbounded concurrency may exhaust memory or sockets long before CPU reaches 100%.

Packaging is therefore connected to the reliability lessons we already learned.

Local Docker success is not production readiness

A successful:

docker build
docker run

proves something useful: the application can be packaged and started.

It does not prove:

  • security,
  • scalability,
  • correct health semantics,
  • disaster recovery,
  • authorization,
  • good observability,
  • safe deployment strategy.

Do not use “Dockerized” as a synonym for “production grade.”

Service Desk connection

Today the Service Desk becomes a releaseable artifact rather than a developer-directory assumption.

The architectural transition is:

Before
------
source code + local Python environment + machine assumptions

After
-----
source
 -> tested build
 -> minimal container image
 -> immutable image identity
 -> runtime configuration/secrets
 -> orchestrated process with health + shutdown behavior

The principle is:

Package the same tested application into a minimal, reproducible, least-privilege image; keep secrets and environment-specific state outside the image; and treat the container as a real production process with explicit health, resources and shutdown behavior.

02 · APPLY

Lesson goal

Today we package the AI Service Desk as a reproducible container image and verify that the image behaves like a real production process.

By the end of the lesson you should be able to:

  • explain image versus container,
  • read a multi-stage Dockerfile,
  • reason about layer cache and build context,
  • keep secrets out of image layers,
  • run the application as a non-root user,
  • explain health and graceful shutdown,
  • identify the exact artifact being promoted,
  • distinguish “Dockerized” from “production ready.”

What changed in the Service Desk today?

Before today, running the project depended on a developer machine having the expected Python environment.

Today we create an immutable deployment artifact:

graph LR
    Git[Source Commit] --> Test[Tests]
    Test --> Build[Docker Build]
    Build --> Image[Image Digest]
    Image --> Scan[Verify / Scan]
    Scan --> Stage[Staging]
    Stage --> Prod[Production Promotion]

The key idea is build once, verify once, promote the same artifact.

Step 1 — Understand the Dockerfile stages

A production-oriented Python image may use separate build and runtime stages:

FROM python:3.13-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

FROM python:3.13-slim AS runtime
WORKDIR /app

RUN useradd --create-home --uid 10001 appuser

COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels

COPY service_desk ./service_desk

USER appuser
EXPOSE 8000
CMD ["uvicorn", "service_desk.main:app", "--host", "0.0.0.0", "--port", "8000"]

The builder may contain temporary build dependencies. The runtime stage should contain only what is needed to execute the service.

Step 2 — Inspect the build context

Docker can only copy files available in the build context.

That means the build context is also a security boundary.

A .dockerignore should normally exclude unnecessary files such as:

.git
.venv
__pycache__
.pytest_cache
.env
local logs
coverage output

Do not depend on .dockerignore as the only secret-control mechanism. Credentials should not be stored in the project tree in the first place.

Step 3 — Understand layer caching

This ordering:

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY service_desk ./service_desk

allows the dependency layer to remain reusable when only application source changes.

If you instead copy the entire project before installing dependencies, every source edit may invalidate that cache layer.

Cache optimization is useful, but correctness wins. If a dependency input changes, the dependency layer must rebuild.

Step 4 — Verify that secrets are absent

Never package .env, cloud credentials, API tokens or private keys into the image.

Also remember that this is unsafe:

COPY secret.txt /tmp/secret.txt
RUN use-secret /tmp/secret.txt
RUN rm /tmp/secret.txt

The secret can remain in an earlier image layer.

Application secrets should normally be supplied at runtime by the deployment platform's secret-management mechanism.

Step 5 — Run without root

Check the effective runtime identity:

docker run --rm ai-service-desk:day38 id

Expected principle:

uid != 0

If the application only works as root, investigate filesystem permissions or privileged operations rather than simply restoring root.

Step 6 — Run the service

docker build -t ai-service-desk:day38 .
docker run --rm -p 8000:8000 ai-service-desk:day38

Then call the service from another terminal.

For example, if the project exposes a health/readiness endpoint:

curl -i http://localhost:8000/health

Verify the endpoint actually represents the health question it claims to answer.

Step 7 — Health checks are not all the same

A production platform may need separate signals for:

  • startup — initialization has completed,
  • readiness — route traffic to this instance now,
  • liveness — restart may be appropriate because the process is unhealthy.

Do not fail liveness merely because one external model provider has a temporary outage unless that truly means this process must be restarted.

Otherwise an upstream outage can trigger a container restart storm.

Step 8 — Test graceful shutdown

Start the container and then stop it:

docker stop ai-service-desk

The service should respond to termination rather than requiring a hard kill.

Conceptually:

SIGTERM
   ↓
stop accepting new work
   ↓
finish/cancel bounded in-flight work
   ↓
close connections + flush telemetry
   ↓
exit

For agent systems, think specifically about in-flight tool calls, streams and durable workflow state.

Step 9 — Check the immutable image identity

Tags such as:

ai-service-desk:latest

are convenient labels, but they can move.

An image digest identifies exact content.

A release pipeline should be able to answer:

Which image digest passed verification and was deployed?

That gives stronger evidence than “production uses latest.”

Step 10 — Build once, configure at runtime

Avoid rebuilding the application image merely to change:

  • API endpoint,
  • environment name,
  • feature configuration,
  • runtime secret.

Prefer:

same image digest
 + staging runtime configuration
 + production runtime configuration

where appropriate.

This reduces “staging tested one artifact but production rebuilt another” drift.

Security checklist

For the Day 38 image, inspect:

  • trusted and maintained base image,
  • pinned/locked Python dependencies,
  • no credentials in layers,
  • non-root runtime user,
  • minimum required files/packages,
  • explicit exposed/listening port,
  • no unnecessary writable directories,
  • runtime secret injection,
  • vulnerability scanning policy,
  • image identity/digest recorded.

A small image is helpful, but “smallest possible” is not the goal if the base is unmaintained or debugging/operations become unsafe.

Failure walkthrough

Image builds locally but fails in CI

Check:

  • files accidentally excluded/included in build context,
  • case-sensitive paths,
  • dependency resolver differences,
  • architecture/platform assumptions,
  • hidden local files the build accidentally relied on.

Container starts and immediately exits

Check:

  • CMD/entrypoint,
  • application exception,
  • required configuration,
  • file permissions for the non-root user,
  • listening host/port.

Container works as root but not as app user

Do not solve this by switching back to root first. Inspect ownership and required filesystem operations.

Deployment constantly restarts during model-provider outage

Inspect health-check semantics. A dependency being unavailable is not automatically proof that restarting the application process will help.

Practical lab

Work in:

service-desk-day-38/

Task A — inspect Dockerfile

For every instruction, write one sentence explaining why it exists.

Task B — inspect .dockerignore

Confirm local environments, Git metadata, test caches and secrets are excluded from the build context where appropriate.

Task C — build and inspect

docker build -t ai-service-desk:day38 .
docker image inspect ai-service-desk:day38

Record the image ID/digest information available in your environment.

Task D — verify non-root execution

Run id in the built image and prove the application runtime user is not UID 0.

Task E — run the application

Start the service and exercise one real Service Desk endpoint.

Task F — stop it gracefully

Observe shutdown logs and confirm the process exits without a forced kill under normal conditions.

Task G — run course tests

cd service-desk-day-38
PYTHONPATH=. pytest -q

Explain what the tests prove—and what they do not prove about the container.

Knowledge check

1. What is the difference between an image and a container?

An image is the packaged immutable artifact; a container is a running process created from that image with runtime configuration/isolation.

2. Why can deleting a secret in a later Dockerfile instruction still be unsafe?

Because the secret may remain in an earlier immutable image layer/history.

3. Why use a multi-stage build?

To separate build-time tooling from the smaller runtime artifact when that separation is useful.

4. Why run as non-root?

It reduces unnecessary process privilege inside the container boundary and limits impact if the application is compromised.

5. Does a successful docker build make the system production-ready?

No. It proves packaging/build success, not authorization, reliability, observability, capacity, security or safe deployment behavior.

Scenario

CI builds image sha256:AAA and tests it. Before production deployment, the pipeline runs docker build again and produces sha256:BBB from a changed dependency mirror. Production deploys BBB.

What is wrong?

Answer: Production is not running the exact artifact that passed verification. Prefer promoting the tested immutable artifact/digest and applying environment-specific runtime configuration separately.

Key takeaways

  • Docker packages the runtime; it does not make the application correct.
  • A container is still a real process with signals, permissions, sockets and resource limits.
  • Keep secrets out of the build context and image layers.
  • Multi-stage builds can reduce runtime dependencies and attack surface.
  • Run with least privilege and deliberate writable paths.
  • Design health checks and shutdown behavior intentionally.
  • Promote the exact artifact that passed verification.

Official references

03 · EXPLAIN

Interview checkpoint

Explain one design decision from this lesson, the alternative you rejected, and the failure mode or evidence that justified your choice.