Image Management

Real Dockerfiles

The project does not use one generic image strategy. It uses three different build patterns because the operational needs are different: a Python + ODBC data pipeline, a multi-stage .NET dashboard, and a live Airflow extension image that layers extra providers onto the official Airflow base image.

Image Definition Inspection

These excerpts are the real build inputs in the repo and on the Airflow VM. Read them before running docker build, because they explain why the images differ in size, package surface, and runtime user model.

Local | Dockerfile | read the Python pipeline image definition

Before rebuilding the pipeline image or diagnosing local/runtime package behavior. It is typically triggered by the pipeline needs a new dependency, ODBC connectivity is failing, or Cloud Run behavior differs from local expectations. This is a file inspection of C:\Users\aperi\DEV\ESG\docker\pipeline.Dockerfile. It is read-only. Show how the project builds the Python pipeline image, installs SQL Server ODBC dependencies, and sets the runtime entrypoint.

The pipeline image is intentionally single-stage because the runtime itself needs the ODBC driver and Python dependencies. It installs Microsoft ODBC Driver 18, copies the pipeline code, sets a non-root user, and starts with ddtrace-run python utils/run_pipeline.py.

Builds a Python 3.12 pipeline image that installs Microsoft ODBC Driver 18, copies the pipeline codebase, switches to appuser, and starts the workload through ddtrace-run python utils/run_pipeline.py.

FROM python:3.12-slim
 
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       curl gnupg2 unixodbc-dev \
    && curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg \
    && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/debian/12/prod bookworm main" \
       > /etc/apt/sources.list.d/mssql-release.list \
    && apt-get update \
    && ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql18 \
    && apt-get clean && rm -rf /var/lib/apt/lists/*
 
WORKDIR /app
 
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
COPY utils/ utils/
COPY ingestion/ ingestion/
COPY db/ db/
COPY data/definitions/ data/definitions/
COPY docker/pipeline-entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
 
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser \
    && chown -R appuser:appuser /app
USER appuser
 
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["ddtrace-run", "python", "utils/run_pipeline.py"]

Local | Dockerfile | read the dashboard image definition

Before rebuilding the dashboard image, before changing its base runtime, or when publish size matters. It is typically triggered by A dashboard dependency changed, a build is slow, or you need to understand why the runtime image is smaller than the SDK image. This is a file inspection of C:\Users\aperi\DEV\ESG\docker\dashboard.Dockerfile. It is read-only. Show how the project compiles the Blazor app in one stage and runs it from a smaller ASP.NET runtime stage.

The dashboard image uses a proper multi-stage build. The heavy SDK layer never ships in the final runtime image, and the runtime stage adds a container healthcheck on /healthz.

Uses a multi-stage .NET 10 build that restores and publishes the Blazor app in the SDK stage, then copies only the published output into a smaller ASP.NET runtime image with a /healthz healthcheck.

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
 
COPY dashboard/ESG.Dashboard/ESG.Dashboard.csproj .
RUN dotnet restore
 
COPY dashboard/ESG.Dashboard/ .
RUN dotnet publish -c Release -o /app/publish
 
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/publish .
 
RUN groupadd -r appuser && useradd -r -g appuser -d /app appuser \
    && chown -R appuser:appuser /app
USER appuser
 
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
 
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -f http://localhost:8080/healthz || exit 1
 
ENTRYPOINT ["dotnet", "ESG.Dashboard.dll"]

Linux | Dockerfile | read the live Airflow VM image extension

Before modifying the Airflow runtime on the VM or when a DAG needs an extra Python package or Airflow provider. It is typically triggered by airflow imports fail, provider packages are missing, or you need to understand what was added on top of the official Airflow image. This is a live file inspection from /home/alexper_recovery_gmail_com/app on stoxx-airflow. It is read-only. Show how the running VM extends apache/airflow:3.2.0 with provider packages while staying pinned to the matching Airflow constraints file.

The VM does not run the stock apache/airflow:3.2.0 image unchanged. It builds stoxx-airflow:3.2.0 locally on the host and layers in Google provider support using the official Airflow constraints URL pattern.

Extends the official apache/airflow:3.2.0 image by copying requirements.txt, computing the matching Airflow constraints URL at build time, and installing the Google provider packages needed by the live DAGs.

FROM apache/airflow:3.2.0
 
COPY requirements.txt /tmp/requirements.txt
 
RUN AIRFLOW_VERSION=$(python -c "from airflow import __version__; print(__version__)") \
    && PYTHON_VERSION=$(python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") \
    && CONSTRAINT_URL="https://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${PYTHON_VERSION}.txt" \
    && pip install --no-cache-dir "apache-airflow==${AIRFLOW_VERSION}" -r /tmp/requirements.txt --constraint "${CONSTRAINT_URL}"
apache-airflow-providers-google
google-cloud-storage==3.10.1

The Airflow extension is intentionally small in code but important in effect. It keeps the base image official and production-oriented, while adding the extra provider and storage client needed by the DAGs on the VM.

ImageBase imageBuild patternRuntime userOperational purpose
stoxx-pipelinepython:3.12-slimSingle-stageappuser in image, overridden to root locally by composePython data pipeline with SQL Server ODBC access and Datadog tracing entrypoint
stoxx-dashboardmcr.microsoft.com/dotnet/sdk:10.0 mcr.microsoft.com/dotnet/aspnet:10.0Multi-stageappuserSmaller runtime image for the Blazor dashboard
stoxx-airflow:3.2.0apache/airflow:3.2.0Base-image extensionAirflow image default 50000Add Google provider packages to the live Airflow stack

Live Build Output

The next question is whether the Dockerfiles actually build cleanly in the current environment. The commands below were run live on April 13, 2026 from C:\Users\aperi\DEV\ESG using Docker Desktop’s BuildKit backend.

Local Image Rebuilds

The local builds are not abstract examples. They are the exact commands used to rebuild current note-specific image tags, and the output below shows the real context transfer size, base image resolution, package installation, and final image export.

PowerShell | docker build | rebuild the local pipeline image

After changing ingestion/, utils/, db/, data/definitions/, requirements.txt, or docker/pipeline-entrypoint.sh. It is typically triggered by A pipeline dependency or runtime behavior changed and you need a fresh local image. PowerShell on the Windows host in C:\Users\aperi\DEV\ESG. This is a state-changing build that creates a new local image tag. Produce a fresh pipeline image and confirm that the ODBC and Python dependency layers still build successfully.

Rebuilds the local Python pipeline image with plain BuildKit progress output.

docker build --progress plain -f docker/pipeline.Dockerfile -t stoxx-pipeline:notes-20260413 .
#0 building with "desktop-linux" instance using docker driver
#4 [internal] load .dockerignore
#4 transferring context: 377B 0.0s done
#5 [internal] load build context
#5 transferring context: 524.43kB 0.1s done
#7 [ 2/13] RUN apt-get update ... ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql18 ...
#7 10.39 Setting up msodbcsql18 (18.6.2.1-1) ...
#10 [ 5/13] RUN pip install --no-cache-dir -r requirements.txt
#10 14.62 Successfully installed beautifulsoup4-4.14.3 ... pyodbc-5.3.0 ... yfinance-1.2.0 ...
#19 exporting manifest sha256:32649470ea25a98a68068616c12253dfe830c3926d1212ea7766432adeb8b154 done
#19 naming to docker.io/library/stoxx-pipeline:notes-20260413 done

The build used BuildKit on the desktop-linux builder, sent a small 524.43kB context thanks to the repo .dockerignore, installed ODBC Driver 18 successfully, and rebuilt the Python dependency layer from requirements.txt. The final image was exported locally under the tag stoxx-pipeline:notes-20260413.

PowerShell | docker build | rebuild the local dashboard image

After changing the dashboard project, the dashboard Dockerfile, or runtime health endpoint behavior. It is typically triggered by the Blazor application changed and you need a fresh local runtime image. PowerShell on the Windows host in C:\Users\aperi\DEV\ESG. This is a state-changing build that creates a new local image tag. Prove that the multi-stage dashboard build still restores, publishes, and exports cleanly.

Rebuilds the local dashboard image with plain BuildKit progress output.

docker build --progress plain -f docker/dashboard.Dockerfile -t stoxx-dashboard:notes-20260413 .
#0 building with "desktop-linux" instance using docker driver
#4 [internal] load .dockerignore
#4 transferring context: 377B done
#5 [internal] load build context
#5 transferring context: 200.25kB 0.0s done
#11 [build 4/6] RUN dotnet restore
#11 1.714   Restored /src/ESG.Dashboard.csproj (in 2.98 sec).
#13 [build 6/6] RUN dotnet publish -c Release -o /app/publish
#13 7.641   ESG.Dashboard -> /src/bin/Release/net10.0/ESG.Dashboard.dll
#13 9.508   ESG.Dashboard -> /app/publish/
#16 exporting manifest sha256:3d09a11b85243b07ddbd185ae9299e136df28dca75398a103ff4248fe5b33539 done
#16 naming to docker.io/library/stoxx-dashboard:notes-20260413 done

This build shows the advantage of the multi-stage design directly. The heavy SDK image is used only in the build stage, the published output is copied into the runtime stage, and the final exported image is a new stoxx-dashboard:notes-20260413 tag without the SDK tooling inside it.

FlagSyntaxDescription
--progressdocker build --progress plain ...Forces BuildKit to emit readable step-by-step plain-text output instead of condensed TTY progress.
-fdocker build -f docker/pipeline.Dockerfile ...Selects the exact Dockerfile instead of defaulting to ./Dockerfile.
-tdocker build -t stoxx-pipeline:notes-20260413 ...Tags the built image with a concrete local name.
.docker build ... .Sets the repo root as the build context.

Live VM Build Boundary

The Airflow VM build path was verified from the live Dockerfile, requirements.txt, container labels, and docker history. The image was not rebuilt on the VM during this documentation pass because rerunning builds on the active orchestration host would mutate a live environment.

Registry And Deployment State

Images only matter operationally when they can be matched to the registry and to the runtime that consumes them. This project currently has a meaningful drift between repo-era registry paths and the live deployment project, so the chapter must record the live state explicitly instead of repeating stale paths.

Image Inventory And Deployment Bindings

This section combines three views: local image inventory, live Artifact Registry tags, and the Cloud Run jobs currently bound to those images. Read the three together. A local image can exist while the registry path has moved, and a registry tag can exist while the runtime has already advanced to a different tag.

FieldSource columnUnit / typeMeaning
REPOSITORY / IMAGEImage repository pathstringThe local or remote image name.
TAG / TAGSMutable image labelstringThe human-readable name attached to the image or digest.
DIGESTRegistry manifest hashstringThe immutable content identity of a remote image.
IMAGE IDLocal image identifierstringThe local image object stored on the Docker host.
CREATE_TIME / UPDATE_TIMERegistry timestampstimestampWhen the remote image entry was created or updated.

PowerShell | docker images | list the local image inventory

Before a rebuild, before cleanup, or when confirming which historical images are still cached on the workstation. It is typically triggered by you need to know what the local Docker host can run immediately without pulling. PowerShell on the Windows host. This is a read-only inventory command. Show which project images and historical registry-tagged images are still present locally.

Lists the current local image inventory that matters to this project.

docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}"
REPOSITORY                                                             TAG           IMAGE ID       SIZE
stoxx-dashboard                                                        latest        fe3194c60e5a   450MB
stoxx-pipeline                                                         latest        f111dc7e04b9   536MB
europe-west1-docker.pkg.dev/stoxx-index-intelligence/stoxx/dashboard   latest        46789c35aea4   444MB
europe-west1-docker.pkg.dev/stoxx-index-intelligence/stoxx/pipeline    latest        10be09be8d6c   484MB

The local Docker host still carries images tagged for the older stoxx-index-intelligence registry path even though the live deployment project is now bq-wh-nb. That is exactly why image notes must always separate local cache state from current deployment truth.

PowerShell / Linux | gcloud artifacts docker images list | list the live Artifact Registry tags

Before a rollout, during incident response, or when a job is using a newer tag than expected. It is typically triggered by you need to know which images actually exist in the live registry today. PowerShell or Linux shell with gcloud authenticated to the live project. This is a read-only registry query. Show the real image tags and digests in the current Artifact Registry repository.

Lists the live registry tags in the current project.

gcloud artifacts docker images list europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo --project bq-wh-nb --include-tags --format="table(IMAGE,DIGEST,TAGS,CREATE_TIME,UPDATE_TIME)"
IMAGE                                                              DIGEST                                                                   TAGS        CREATE_TIME          UPDATE_TIME
europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-bronze-load  sha256:24e74f8af8fb6a90045a927a1cf84ff65882ee564dab8b4cca07e95c9991eede  20260413-1  2026-04-13T17:15:53  2026-04-13T17:15:53
europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-serving      sha256:ae50c19b7696ab3275310a0f58e74674719938cfcdc49aa9852f4bbb51efbc63  20260413-6  2026-04-13T19:18:45  2026-04-13T19:18:45
europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-stage-fetch  sha256:f14039eafdf2e053d0656882822f962f778f5e8cb03bde6258df5899b2336e81  20260413-2  2026-04-13T17:54:56  2026-04-13T17:54:56
europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-transforms   sha256:5a46e2a1c6da0e4bf718fb92edd9296769c912f3a959020235bbf82d4d7dabd7  20260413-1  2026-04-13T18:00:27  2026-04-13T18:00:27

This is the authoritative current registry path. The live project is bq-wh-nb, the repo is stoxx-demo, and stoxx-serving has already moved through multiple same-day tags, with 20260413-6 being the newest tag at the time of capture.

PowerShell / Linux | gcloud run jobs list | list the Cloud Run jobs currently bound to those images

After a push, during rollback planning, or when a job appears to be using the wrong code version. It is typically triggered by the registry contains several tags and you need to know which one the platform is actually executing. PowerShell or Linux shell with gcloud. This is a read-only control-plane query. Bind each live Cloud Run job to its current image reference and latest execution state.

Shows the current image-to-job bindings in Cloud Run.

gcloud run jobs list --project bq-wh-nb --region europe-west1 --format="table(metadata.name,spec.template.spec.template.spec.containers[0].image,status.latestCreatedExecution.name,status.latestCreatedExecution.completionStatus)"
NAME               IMAGE                                                                         LATEST_CREATED_EXECUTION_NAME  COMPLETION_STATUS
stoxx-bronze-load  europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-bronze-load:20260413-1  stoxx-bronze-load-47gpz        EXECUTION_SUCCEEDED
stoxx-serving      europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-serving:20260413-6      stoxx-serving-rtrgv            EXECUTION_RUNNING
stoxx-stage-fetch  europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-stage-fetch:20260413-2  stoxx-stage-fetch-4sxtt        EXECUTION_SUCCEEDED
stoxx-transforms   europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-transforms:20260413-1   stoxx-transforms-klwrf         EXECUTION_SUCCEEDED

This output closes the loop. The registry and the runtime agree on the bq-wh-nb/stoxx-demo path, and the currently running stoxx-serving job is pinned to 20260413-6, not to any older repo-era stoxx-index-intelligence path.

Problem: The repo still contains deployment references to stoxx-index-intelligence, but the live deployment estate on April 13, 2026 uses bq-wh-nb. Relying on the repo value would point operators at the wrong registry and the wrong deployment project. Diagnosis: The codebase contains stale infrastructure and workflow references from an earlier project identifier. Resolution: This chapter uses the live bq-wh-nb registry and job bindings as the current operational truth and treats the repo value as historical drift. Validation: The registry output and the Cloud Run job output both resolve to europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/.... Prevention rule: Verify project ID, registry path, and bound runtime image with live gcloud queries before documenting or rolling out image changes.

FlagSyntaxDescription
--include-tagsgcloud artifacts docker images list ... --include-tagsIncludes tag names instead of returning only digests.
--projectgcloud ... --project bq-wh-nbForces the query to the live GCP project.
--regiongcloud run jobs list --region europe-west1Targets the region that hosts the current Cloud Run jobs.
--formatgcloud ... --format="table(...)"Projects only the fields needed for operational inspection.

Layer And Hygiene Guidance

The live Airflow VM image is useful because it shows what a tightly scoped extension layer looks like. The local workstation is useful because it shows the opposite side of the image-management story: how old images, old tags, volumes, and build cache stay behind unless someone cleans them deliberately.

Layer Inspection And Build Hygiene

The Airflow VM did not need a completely new image from scratch. It extended the official Airflow 3.2.0 image with a small additional layer that installs provider packages under the matching constraints file. The history output below shows that delta directly.

Linux | docker history | inspect the live Airflow image history

After changing the VM Dockerfile, after a failed provider import, or when you need to know whether the custom layer is broad or narrowly scoped. It is typically triggered by the running Airflow containers behave differently from the stock base image and you need to identify what was actually added. Linux shell on stoxx-airflow. This is a read-only image inspection. Measure the real custom layer on top of the official Airflow base image.

Shows the top of the live Airflow image history on the VM.

gcloud compute ssh stoxx-airflow --project bq-wh-nb --zone europe-west1-b --tunnel-through-iap --command "docker history --no-trunc --human stoxx-airflow:3.2.0"
IMAGE                                                                     CREATED    CREATED BY                                                                                                                                                                                                 SIZE
sha256:3b3ecc4b047b2ce26ac6e7335ff54a0a16538d28903b8d1e43d84626e861583b   3 hours ago RUN /bin/bash -o pipefail -o errexit -o nounset -o nolog -c AIRFLOW_VERSION=$(python -c "from airflow import __version__; print(__version__)") ... pip install --no-cache-dir "apache-airflow==${AIRFLOW_VERSION}" -r /tmp/requirements.txt --constraint "${CONSTRAINT_URL}" # buildkit   27.9MB
<missing>                                                                 3 hours ago COPY requirements.txt /tmp/requirements.txt # buildkit                                                                                                                                                     12.3kB
<missing>                                                                 6 days ago  ENTRYPOINT ["/usr/bin/dumb-init" "--" "/entrypoint"]                                                                                                                                                        0B

This is a compact and healthy customization pattern. The meaningful project-specific delta is a 27.9MB pip install layer and a tiny requirements.txt copy layer. Everything else remains inherited from the official Airflow image, which reduces drift and makes upgrades easier than rebuilding Airflow from scratch.

Local | .dockerignore | verify the real build-context boundary on the local host

Before enlarging the build context or after a suspiciously slow local build. It is typically triggered by build time grows unexpectedly or files appear in the image that should never have been part of the context. File inspection of C:\Users\aperi\DEV\ESG\.dockerignore. This is read-only. Show which files are intentionally kept out of local builds.

The local .dockerignore is doing real work. It excludes Git metadata, Python caches, local data directories, logs, markdown docs, and environment files, which is why the live local build contexts above stayed below one megabyte.

Shows that the local build context excludes Git metadata, Python caches, runtime data directories, logs, markdown files, and environment files, which is why the recorded BuildKit transfers stayed well under one megabyte.

# Git
.git
.gitignore
 
# Python
__pycache__
*.pyc
.venv
venv
 
# Data (fetched at runtime)
data/dimensions
data/ohlcv
data/signals
data/pulse
data/tickers
logs
 
# Docker
docker-compose.yml
.dockerignore
 
# Misc
*.md
.env
.env.example

This file directly explains the 377B .dockerignore transfer and the small context sizes recorded in the live builds. It also keeps secrets such as .env out of the build context, which matters because the raw local docker compose config output did interpolate live secret values on the workstation.

FlagSyntaxDescription
--no-truncdocker history --no-trunc <image>Prints the full build command instead of clipping it.
--humandocker history --human <image>Renders layer sizes in readable units such as 27.9MB.
--projectgcloud compute ssh ... --project bq-wh-nbForces the remote image inspection to the live project.
--zonegcloud compute ssh ... --zone europe-west1-bReaches the correct VM zone.
--tunnel-through-iapgcloud compute ssh ... --tunnel-through-iapUses IAP for remote host access.
--commandgcloud compute ssh ... --command "<cmd>"Runs the image inspection non-interactively on the host.