DEV Community

Cover image for Docker for Data Engineers: Dockerfiles for dbt, Airflow, Spark, Notebooks & Best Practices
Gowtham Potureddi
Gowtham Potureddi

Posted on

Docker for Data Engineers: Dockerfiles for dbt, Airflow, Spark, Notebooks & Best Practices

docker for data engineers is the containerisation surface every DE platform team eventually owns — the dbt image that runs a nightly job in the CI runner and again on the Kubernetes worker, the Airflow image with the extra provider packages the team needs but the official image doesn't ship, the Spark driver image whose Java version has to exactly match the executor's, the JupyterLab image analysts use for exploration. Every DE eventually writes a Dockerfile; knowing multi-stage builds (builder + runtime split), non-root users, .dockerignore hygiene, layer-cache ordering, HEALTHCHECK for K8s liveness, ENTRYPOINT exec-form for signal handling, the Airflow constraints-file pattern that prevents provider dependency conflicts, and BuildKit for parallel stage builds is what separates a mid-level operator who ships fragile 2 GB images from a senior operator who ships lean 200 MB images with clean CI cycles.

The tour walks the five pillars — (1) production Dockerfile for dbt with multi-stage builder + non-root user + profiles at runtime, (2) custom Airflow images extending the official image with pinned providers via the constraints URL matching your Airflow version, (3) Dockerfiles for Spark driver + JupyterLab notebook environments including the OpenJDK layer and mamba for reproducible Python envs, (4) the multi-stage build pattern that separates fat builder from lean runtime + the .dockerignore / layer-cache-order rules that speed rebuilds 10×, and (5) production best practices — HEALTHCHECK, ENTRYPOINT ["exec"] for SIGTERM handling, EXPOSE documentation, ARG vs ENV, secrets via env-only, trivy scanning, cosign signing. Every section ships a Solution-Tail interview answer — code, trace, output, why-this-works with __concept__ underlines.

PipeCode blog header for Docker for Data Engineers — bold white headline 'DOCKER' with subtitle 'dbt · Airflow · Spark · Notebooks' and a stylised container ship glyph carrying four coloured containers with Dockerfile scrolls on a dark gradient with pipecode.ai attribution.

Practice on SQL library →, SQL optimization drills →, and SQL indexing drills →.


On this page


1. Why Docker matters for DE in 2026

The docker for data engineers mental model — reproducible environments, small images, layer caching, non-root, signals

The one-sentence invariant: a production Docker image is small (< 300 MB for a Python DE image, < 1 GB for Spark), reproducible (pinned versions everywhere, no latest tags), safe (non-root user, no baked secrets), fast to rebuild (dependencies before source code so cache invalidation is minimal), and correct under Kubernetes (HEALTHCHECK, exec-form ENTRYPOINT for SIGTERM handling); every DE image you ship should hit those five properties or the on-call engineer will pay the price at 3 a.m.

Where Docker actually shows up in DE.

  • Airflow / Prefect operators. Each task is a container; the image ships the DAG's dependencies.
  • dbt Cloud alternatives. Self-hosted dbt in container; scheduled via Airflow or Argo.
  • Spark on K8s. SparkApplication CRD launches driver + executor containers.
  • CI test isolation. Reproducible env per test run; no "works on my machine".
  • Notebook environments. JupyterLab for analysts; kernel = data-engineer-blessed image.
  • Local dev. docker-compose up spins Postgres + Airflow + dbt + Metabase for feature development.

The five properties of a good DE image.

  • Small. python:3.11-slim (150 MB) not python:3.11 (900 MB) not python:3.11-alpine (musl compat issues on numpy/pandas).
  • Reproducible. Pin versions of everything (dbt-core==1.9.0, snowflake-connector-python==3.13.0); use constraints files where relevant (Airflow).
  • Safe. useradd -u 1000 appuser + USER appuser; never run as root; never bake secrets into layers.
  • Fast to rebuild. COPY requirements before source; multi-stage builder + runtime; .dockerignore excludes bulky files.
  • K8s-friendly. HEALTHCHECK, ENTRYPOINT ["exec"] (for SIGTERM propagation), EXPOSE (docs), resource-aware.

The anti-patterns.

  • FROM ubuntu:latest. Latest tag = non-reproducible; ubuntu-full is bloated.
  • Running as root. Security violation; K8s PSPs reject.
  • No .dockerignore. Copies .git, .venv, __pycache__, node_modules — inflates image + slow build.
  • Single monolithic layer. COPY . . before pip install — every source change invalidates deps cache.
  • ENTRYPOINT "cmd". Shell form; doesn't propagate SIGTERM; K8s can't gracefully stop.
  • Embedded secrets. ENV AWS_ACCESS_KEY=... — visible in layer history forever.
  • pip install without version pins. Every rebuild = different versions = broken pipelines.

What senior interviewers actually probe on Docker for DE.

  • Multi-stage builds. Builder + runtime; why final image is small.
  • .dockerignore contents. .git, .venv, __pycache__, *.pyc, .pytest_cache, node_modules.
  • Layer cache order. COPY requirements first, install deps, then COPY source.
  • Non-root user. useradd -m -u 1000 appuser + USER appuser.
  • HEALTHCHECK. Simple command verifying process is alive.
  • ENTRYPOINT exec form. ["python", "main.py"] not "python main.py".
  • Airflow constraints file. The URL matching your Airflow + Python version.
  • Base image choice. slim vs alpine vs distroless.
  • Image size. Aim < 300 MB Python; < 1 GB Spark.
  • Signal handling. SIGTERM to child; tini as init for zombies.
  • BuildKit. DOCKER_BUILDKIT=1 for parallel stages.
  • trivy scan. Vulnerability scanning in CI.

Worked example — the 2 GB image that shrunk to 250 MB

Detailed explanation. A dbt image at v1 was 2.1 GB — built from python:3.11 (900 MB) + all dev tools + tests + docs. After applying five best practices, dropped to 250 MB.

Question. Show before + after.

Code — Before (2.1 GB).

FROM python:3.11

WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
RUN pip install -r requirements-dev.txt

CMD python main.py
Enter fullscreen mode Exit fullscreen mode

Code — After (250 MB).

# ---- Builder stage ----
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# ---- Runtime stage ----
FROM python:3.11-slim
RUN useradd -m -u 1000 dbtuser
COPY --from=builder --chown=dbtuser:dbtuser /root/.local /home/dbtuser/.local
ENV PATH=/home/dbtuser/.local/bin:$PATH

USER dbtuser
WORKDIR /app
COPY --chown=dbtuser:dbtuser dbt_project /app

HEALTHCHECK --interval=30s --timeout=5s CMD dbt --version || exit 1
ENTRYPOINT ["dbt"]
CMD ["run"]
Enter fullscreen mode Exit fullscreen mode

Step-by-step explanation.

  1. python:3.11python:3.11-slim — saves ~700 MB (Debian slim vs full).
  2. Multi-stage — builder installs deps; runtime only copies wheels; dev deps never in runtime.
  3. --no-cache-dir — pip skips ~200 MB of package caches.
  4. .dockerignore (implied) — excludes .git, tests, docs.
  5. Non-root user — sec best practice; K8s PSP-compatible.
  6. HEALTHCHECK — K8s liveness probe honours.
  7. ENTRYPOINT ["dbt"] exec form — SIGTERM propagates cleanly.

Output.

Attribute Before After
Size 2.1 GB 250 MB
Base image python:3.11 python:3.11-slim
Non-root no yes
Multi-stage no yes
Rebuild speed (source-only change) 3 min 15 sec

Rule of thumb. Every DE image should be < 300 MB (Python) or < 1 GB (Spark). Multi-stage + slim base is the biggest win.

Worked example — layer cache ordering

Detailed explanation. Wrong order: COPY . . RUN pip install — every source change invalidates the pip layer. Right order: COPY requirements.txt . RUN pip install . COPY . . — pip layer stays cached until requirements change.

Code.

# BAD — pip re-runs on every code change
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt

# GOOD — pip cached until requirements.txt changes
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. Slowest-changing (dependencies) copied first; fastest-changing (source) last.

Worked example — signal handling

Detailed explanation. Kubernetes sends SIGTERM to the container; shell-form ENTRYPOINT doesn't propagate to the child process; process ignores SIGTERM; K8s waits 30 sec then SIGKILL. Data loss possible.

Code.

# BAD — shell form; SIGTERM not propagated
ENTRYPOINT python main.py

# GOOD — exec form; SIGTERM goes to python directly
ENTRYPOINT ["python", "main.py"]

# BEST — tini as init to reap zombies
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["tini", "--", "python", "main.py"]
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. Always exec-form ENTRYPOINT. tini for long-running processes to reap zombies.

Common beginner mistakes

  • FROM latest — no version reproducibility.
  • Root user — security violation.
  • No .dockerignore — image bloat + slow builds.
  • COPY source before deps — cache thrash.
  • Shell form ENTRYPOINT — signal-handling broken.
  • Baking secrets — visible forever in layer history.
  • No HEALTHCHECK — K8s can't tell liveness.
  • Alpine base for numpy/pandas — musl compat issues.

docker for data engineers interview question on image size + build speed

A senior interviewer opens with: "Your dbt image is 2 GB and takes 5 minutes to rebuild on every source change. Optimise both."

Solution Using multi-stage + slim base + .dockerignore + BuildKit + layer ordering

# syntax=docker/dockerfile:1.6
# ---- Builder stage ----
FROM python:3.11-slim AS builder

WORKDIR /app

# Deps first (cached until requirements change)
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --user --no-cache-dir -r requirements.txt

# ---- Runtime stage ----
FROM python:3.11-slim

RUN useradd -m -u 1000 dbtuser
COPY --from=builder --chown=dbtuser:dbtuser /root/.local /home/dbtuser/.local
ENV PATH=/home/dbtuser/.local/bin:$PATH

USER dbtuser
WORKDIR /app

# Source last (invalidates less often)
COPY --chown=dbtuser:dbtuser dbt_project /app

HEALTHCHECK --interval=30s --timeout=5s CMD dbt --version || exit 1
ENTRYPOINT ["dbt"]
CMD ["run"]
Enter fullscreen mode Exit fullscreen mode

Companion .dockerignore.

.git/
.venv/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
.ruff_cache/
node_modules/
docs/
tests/
*.log
Enter fullscreen mode Exit fullscreen mode

Build with BuildKit.

DOCKER_BUILDKIT=1 docker build -t myco/dbt:1.9.0 .
Enter fullscreen mode Exit fullscreen mode

Step-by-step trace.

Step Action Layer change
1 slim base 150 MB
2 pip install requirements 100 MB (cached)
3 Copy source Small (invalidates only on source change)
4 Multi-stage discards builder Final image ~250 MB
5 BuildKit parallel + cache mount 15-30 sec rebuild

Output:

Metric Before After
Image size 2 GB 250 MB
First build 5 min 3 min
Source-only rebuild 5 min 15 sec
Deps-changed rebuild 5 min 45 sec

Why this works — concept by concept:

  • Multi-stage — builder + runtime; dev tools never in runtime.
  • Slim base — 150 MB Debian slim, compatible with pandas/numpy.
  • Layer order — deps before source; cache stays valid on source-only changes.
  • .dockerignore — excludes .git, .venv, tests; smaller build context; faster upload.
  • BuildKit cache mount — pip cache preserved across builds.
  • Non-root — K8s PSP-compatible.
  • HEALTHCHECK — K8s liveness.
  • ENTRYPOINT exec form — clean SIGTERM handling.

SQL
Topic — SQL
SQL practice library

Practice →

SQL Topic — optimization SQL optimization drills

Practice →


2. Dockerfile for dbt

dockerfile dbt — multi-stage build with pinned dbt-core + adapters + profiles.yml at runtime

The mental model in one line: a production dbt Docker image installs dbt-core plus the target-specific adapter(s) (dbt-postgres, dbt-snowflake, dbt-bigquery) with pinned versions in a builder stage, copies the wheels into a slim runtime stage as a non-root user, mounts profiles.yml at runtime (never bake credentials), sets DBT_PROFILES_DIR env, and exposes a HEALTHCHECK that verifies dbt is invokable — the image is ~250 MB and rebuilds in 15 seconds on source-only changes.

Visual diagram of Dockerfile for dbt — builder stage card with python-slim + pip install dbt-core adapters, runtime stage card with slim base non-root COPY from builder profiles volume-mount glyph HEALTHCHECK ribbon; on a light PipeCode card.

Slot 1 — the canonical dbt Dockerfile.

# syntax=docker/dockerfile:1.6

# ---- Builder ----
FROM python:3.11-slim AS builder

# Install build deps if needed for compilation
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Pin exact versions
RUN pip install --user --no-cache-dir \
    dbt-core==1.9.0 \
    dbt-postgres==1.9.0 \
    dbt-snowflake==1.9.0 \
    dbt-bigquery==1.9.0

# ---- Runtime ----
FROM python:3.11-slim

# Non-root user
RUN useradd -m -u 1000 dbtuser

COPY --from=builder --chown=dbtuser:dbtuser /root/.local /home/dbtuser/.local
ENV PATH=/home/dbtuser/.local/bin:$PATH

USER dbtuser
WORKDIR /app

# Copy dbt project (models, macros, dbt_project.yml)
COPY --chown=dbtuser:dbtuser dbt_project /app

# profiles.yml mounted at runtime; never baked in
ENV DBT_PROFILES_DIR=/app/.dbt

# Healthcheck
HEALTHCHECK --interval=30s --timeout=5s CMD dbt --version || exit 1

ENTRYPOINT ["dbt"]
CMD ["run"]
Enter fullscreen mode Exit fullscreen mode

Slot 2 — pinning versions.

  • dbt-core==1.9.0 — exact version.
  • Adapter version must match core (dbt-postgres==1.9.0).
  • Don't use >=1.9.0 — future patch might break.
  • Test versions in staging before pinning.

Slot 3 — profiles.yml at runtime.

# Mount profiles as volume
docker run -v ./.dbt:/app/.dbt myco/dbt:1.9.0 run

# Or via K8s
volumeMounts:
  - name: profiles
    mountPath: /app/.dbt
Enter fullscreen mode Exit fullscreen mode

Credentials come from env vars referenced in profiles.yml:

# profiles.yml
myproject:
  target: prod
  outputs:
    prod:
      type: snowflake
      account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
      user: "{{ env_var('SNOWFLAKE_USER') }}"
      password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
Enter fullscreen mode Exit fullscreen mode

Slot 4 — dbt commands.

# Full build
docker run myco/dbt:1.9.0 build

# Specific model
docker run myco/dbt:1.9.0 run --select fact_orders+

# Deps + build
docker run myco/dbt:1.9.0 deps
docker run myco/dbt:1.9.0 build

# Test only
docker run myco/dbt:1.9.0 test
Enter fullscreen mode Exit fullscreen mode

Slot 5 — running in K8s (KubernetesPodOperator).

from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator

dbt_build = KubernetesPodOperator(
    task_id="dbt_build",
    image="myco/dbt:1.9.0",
    cmds=["dbt"],
    arguments=["build", "--select", "fact_orders+"],
    env_vars={"SNOWFLAKE_ACCOUNT": "...", "SNOWFLAKE_USER": "..."},
    volumes=[{...profiles...}],
    volume_mounts=[{...mount to /app/.dbt...}],
)
Enter fullscreen mode Exit fullscreen mode

Slot 6 — dbt deps caching.

# Cache dbt deps if you use packages
COPY packages.yml /app/
RUN dbt deps
Enter fullscreen mode Exit fullscreen mode

Slot 7 — target-schema pattern.

  • Bake environment-neutral image.
  • Environment-specific config via env vars.
  • Same image runs in dev/staging/prod.

Slot 8 — logging.

  • dbt writes logs to logs/dbt.log by default.
  • Configure to stdout for K8s pod logs.
  • dbt --log-format json for structured logging.

Slot 9 — CI usage.

# GitHub Actions
- name: Build dbt image
  run: docker build -t myco/dbt:${{ github.sha }} .

- name: Run dbt tests
  run: docker run --env SNOWFLAKE_PASSWORD myco/dbt:${{ github.sha }} test
Enter fullscreen mode Exit fullscreen mode

Slot 10 — dbt state comparison (slim CI).

# Run only changed models
docker run \
  -v ./state:/app/state \
  myco/dbt:1.9.0 build --select state:modified+ --state /app/state
Enter fullscreen mode Exit fullscreen mode

Common beginner mistakes

  • Baking profiles.yml with credentials — visible in layers.
  • Using dbt-core unpinned — surprise upgrades.
  • Running as root — security violation.
  • Not COPYing dbt_project.yml — image can't identify project.
  • Missing HEALTHCHECK — K8s can't verify.
  • Alpine base with numpy — musl compat issues.

Worked example — build + test in CI

Code.

# .github/workflows/dbt.yml
name: dbt CI
on: [pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: |
          DOCKER_BUILDKIT=1 docker build \
            -t myco/dbt:${{ github.sha }} \
            .

      - name: Run dbt tests
        env:
          SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
          SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}
          SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}
        run: |
          docker run --rm \
            -e SNOWFLAKE_ACCOUNT \
            -e SNOWFLAKE_USER \
            -e SNOWFLAKE_PASSWORD \
            -v ${{ github.workspace }}/profiles:/app/.dbt \
            myco/dbt:${{ github.sha }} \
            test
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. CI builds fresh image; tests against ephemeral schema; credentials via secrets.

Worked example — running as sidecar in Airflow

Code.

KubernetesPodOperator(
    task_id="dbt_daily",
    image="myco/dbt:1.9.0",
    cmds=["dbt"],
    arguments=["build"],
    env_vars={
        "SNOWFLAKE_ACCOUNT": Variable.get("sf_account"),
        "SNOWFLAKE_USER": Variable.get("sf_user"),
        "SNOWFLAKE_PASSWORD": Variable.get("sf_pw"),
    },
    volumes=[
        k8s.V1Volume(
            name="profiles",
            config_map=k8s.V1ConfigMapVolumeSource(name="dbt-profiles"),
        )
    ],
    volume_mounts=[
        k8s.V1VolumeMount(name="profiles", mount_path="/app/.dbt")
    ],
    is_delete_operator_pod=True,   # cleanup on success/failure
    get_logs=True,
)
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. Profiles.yml in ConfigMap; secrets in Kubernetes Secrets referenced via env_vars.

Worked example — slim CI with state:modified+

Code.

- name: Run only changed models
  run: |
    docker run --rm \
      -v ./state:/app/state \
      myco/dbt:latest \
      build --select state:modified+ --defer --state /app/state
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. dbt slim CI runs only changed models + downstream; huge speedup on large repos.

dockerfile dbt interview question on state management

A senior interviewer asks: "Your dbt repo has 500 models. CI runs the full build on every PR — takes 30 minutes. How do you slim?"

Solution Using dbt slim CI with state comparison + image caching

- name: Restore prod state
  uses: actions/download-artifact@v3
  with:
    name: prod-dbt-manifest
    path: ./state

- name: Build image with cache
  run: |
    DOCKER_BUILDKIT=1 docker build \
      --cache-from myco/dbt:cache \
      --cache-to myco/dbt:cache \
      -t myco/dbt:${{ github.sha }} \
      .

- name: Run only changed
  run: |
    docker run --rm \
      -v ./state:/app/state \
      myco/dbt:${{ github.sha }} \
      build --select state:modified+ --defer --state /app/state
Enter fullscreen mode Exit fullscreen mode

Why this works — concept by concept:

  • Prod manifest artifact — the state file from prod's last successful run.
  • state:modified+ selector — only models changed or downstream of changed.
  • --defer — unchanged upstream models come from prod state, not rebuilt.
  • Image cache — BuildKit cache-from/to speeds image rebuilds.
  • Cost — 30 min → 3 min for typical PRs.

SQL
Topic — SQL
SQL practice library

Practice →

SQL Topic — optimization SQL optimization drills

Practice →


3. Dockerfile for Airflow

dockerfile airflow — extend the official image with providers via constraints.txt

The mental model in one line: never build Airflow from scratch — always extend the official apache/airflow:VERSION-pythonVERSION image, install additional Python providers via pip install --constraint <constraints-url> using the URL matching your Airflow version + Python minor version (https://raw.githubusercontent.com/apache/airflow/constraints-2.10.0/constraints-3.11.txt) to prevent dependency conflicts, bake DAGs into the image OR mount from git-sync sidecar, use non-root airflow user (already in official image), and add HEALTHCHECK for K8s liveness.

Visual diagram of Dockerfile for Airflow — official image card apache/airflow:2.10.0-python3.11 with extension glyph, constraints URL card hyperlink glyph pointing to constraints-2.10.0/3.11.txt, providers card with snowflake postgres dbt chips; on a light PipeCode card.

Slot 1 — the canonical Airflow Dockerfile.

FROM apache/airflow:2.10.0-python3.11

# Switch to root for system deps
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    git \
    && rm -rf /var/lib/apt/lists/*

# Switch back to airflow user for pip install
USER airflow

# Install providers with constraints to avoid conflicts
ARG AIRFLOW_VERSION=2.10.0
ARG PYTHON_VERSION=3.11
RUN pip install --no-cache-dir \
    "apache-airflow-providers-snowflake" \
    "apache-airflow-providers-postgres" \
    "apache-airflow-providers-amazon" \
    "apache-airflow-providers-cncf-kubernetes" \
    "dbt-core==1.9.0" \
    "dbt-snowflake==1.9.0" \
    --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${PYTHON_VERSION}.txt"

# Copy DAGs (or use git-sync at runtime)
COPY --chown=airflow:root dags /opt/airflow/dags
COPY --chown=airflow:root plugins /opt/airflow/plugins
Enter fullscreen mode Exit fullscreen mode

Slot 2 — the constraints file explained.

  • Airflow ships a constraints-VERSION/constraints-PYVER.txt per release.
  • Pins every dep to a compatible version tested with Airflow release.
  • Without it, pip resolves to latest and conflicts silently.
  • URL format: https://raw.githubusercontent.com/apache/airflow/constraints-{AIRFLOW}/constraints-{PY}.txt.

Slot 3 — DAG deployment options.

  • Bake in image. Copy DAGs into image at build. Simple; requires rebuild per DAG change.
  • git-sync sidecar. Sidecar container clones repo into shared volume; DAGs sync every 60s. Best for GitOps.
  • S3 sync. Similar to git-sync but from S3; less common.

Slot 4 — configuration via env vars.

ENV AIRFLOW__CORE__EXECUTOR=KubernetesExecutor
ENV AIRFLOW__CORE__LOAD_EXAMPLES=False
ENV AIRFLOW__WEBSERVER__RBAC=True
Enter fullscreen mode Exit fullscreen mode

Airflow config values via AIRFLOW__<section>__<key> env pattern.

Slot 5 — custom plugins.

COPY --chown=airflow:root plugins /opt/airflow/plugins
Enter fullscreen mode Exit fullscreen mode

Custom operators, hooks, sensors.

Slot 6 — providers matrix.

  • apache-airflow-providers-snowflake — Snowflake connections + operators.
  • apache-airflow-providers-postgres — Postgres.
  • apache-airflow-providers-amazon — AWS (S3, Redshift, Athena, Glue).
  • apache-airflow-providers-google — GCP (BigQuery, GCS).
  • apache-airflow-providers-microsoft-azure — Azure.
  • apache-airflow-providers-dbt-cloud — dbt Cloud.
  • apache-airflow-providers-cncf-kubernetes — Kubernetes.

Slot 7 — DAG image vs image-per-task.

  • Single image with all DAGs — simpler; larger image.
  • Image per task — KubernetesPodOperator + task-specific image (dbt, spark, etc.); best isolation.

Slot 8 — building with local test.

DOCKER_BUILDKIT=1 docker build -t myco/airflow:2.10.0 .

# Verify
docker run --rm myco/airflow:2.10.0 airflow version
Enter fullscreen mode Exit fullscreen mode

Slot 9 — providers-only vs full stack.

Alternative: use apache/airflow and mount providers via values.yaml on Helm chart. Less flexibility; simpler.

Slot 10 — DAG parsing at startup.

  • Airflow parses DAGs on scheduler start.
  • Bad DAG = scheduler crash.
  • CI check: python -c "import dag_module" on every DAG.
  • airflow dags list in build validates parsing.

Common beginner mistakes

  • Not using constraints URL — provider conflicts.
  • Root user for DAGs — permission issues.
  • Baking secrets — visible in layer history.
  • Wrong Python version in constraints URL.
  • apache-airflow in pip (main package) — should extend official image.
  • Forgetting --constraint — pip resolves latest.

Worked example — extending Airflow with custom providers

Code.

FROM apache/airflow:2.10.0-python3.11

USER airflow

ARG AIRFLOW_VERSION=2.10.0
ARG PYTHON_VERSION=3.11
ARG CONSTRAINT_URL=https://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${PYTHON_VERSION}.txt

RUN pip install --no-cache-dir \
    "apache-airflow-providers-snowflake" \
    "apache-airflow-providers-postgres" \
    "apache-airflow-providers-amazon" \
    "dbt-core==1.9.0" \
    "dbt-snowflake==1.9.0" \
    "pandas==2.1.4" \
    --constraint "${CONSTRAINT_URL}"

COPY --chown=airflow:root dags /opt/airflow/dags
COPY --chown=airflow:root plugins /opt/airflow/plugins

ENV AIRFLOW__CORE__LOAD_EXAMPLES=False
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. Constraints URL is mandatory for stability.

Worked example — validating DAGs at build

Code.

# At end of Dockerfile
RUN airflow db init && \
    airflow dags list-import-errors && \
    if [ "$?" != "0" ]; then exit 1; fi
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. Fail CI on DAG parse errors.

Worked example — GitOps with git-sync sidecar

Helm values.yaml.

dags:
  gitSync:
    enabled: true
    repo: git@github.com:myco/airflow-dags.git
    branch: main
    subPath: dags
    interval: 60
    knownHosts: |
      github.com ssh-rsa AAAAB...
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. git-sync for GitOps; image build for immutable deploys.

dockerfile airflow interview question on constraints

A senior interviewer asks: "What's the constraints-file pattern for Airflow, and what breaks without it?"

Solution Using constraints URL matching Airflow + Python version

ARG AIRFLOW_VERSION=2.10.0
ARG PYTHON_VERSION=3.11
RUN pip install --no-cache-dir \
    "apache-airflow-providers-snowflake" \
    "apache-airflow-providers-postgres" \
    --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${PYTHON_VERSION}.txt"
Enter fullscreen mode Exit fullscreen mode

Why this works — concept by concept:

  • Constraints file — Airflow's tested-compatible versions of every dep.
  • Without it — pip resolves latest; provider vX.Y incompatible with core vA.B.
  • Symptom — silent version conflicts; scheduler crashes; ImportError at runtime.
  • Fix — always pass --constraint URL when installing providers.

SQL
Topic — SQL
SQL practice library

Practice →

SQL Topic — indexing SQL indexing drills

Practice →


4. Dockerfile for Spark / notebook

dockerfile spark — driver image matching cluster Java version; JupyterLab for analysts

The mental model in one line: Spark Docker images extend apache/spark:VERSION-pythonN for the driver, install pyspark + Delta / Iceberg libraries pinned to the Spark version, run as non-root spark user, and match the Java version to the K8s / EMR / Databricks cluster exactly — mismatched Java = runtime NoClassDefFoundError; JupyterLab images extend jupyter/pyspark-notebook for analysts + install Polars, DuckDB, Pydantic via mamba for reproducible env.

Visual diagram of Dockerfile for Spark and notebook — Spark card with apache/spark base and pyspark install non-root spark user, Jupyter card with jupyter/pyspark-notebook base and mamba install duckdb polars pydantic; on a light PipeCode card.

Slot 1 — Spark driver image.

FROM apache/spark:3.5.1-python3

USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

USER spark

RUN pip install --no-cache-dir \
    pyspark==3.5.1 \
    delta-spark==3.2.0 \
    pandas==2.1.4 \
    polars==0.20.0 \
    boto3

WORKDIR /opt/spark/work-dir

# Copy application
COPY --chown=spark:spark app /opt/spark/work-dir

ENTRYPOINT ["/opt/entrypoint.sh"]
Enter fullscreen mode Exit fullscreen mode

Slot 2 — Java version match.

  • Spark 3.5 → Java 17.
  • Spark 3.4 → Java 11 or 17.
  • Cluster Java must match driver Java for serialised UDFs.
  • apache/spark:3.5.1-python3 ships Java 17.

Slot 3 — SparkApplication CRD (K8s).

apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
spec:
  image: myco/spark:3.5.1
  mainApplicationFile: local:///opt/spark/work-dir/main.py
  sparkVersion: 3.5.1
  driver:
    cores: 1
    memory: 2g
    serviceAccount: spark
  executor:
    instances: 5
    cores: 2
    memory: 4g
Enter fullscreen mode Exit fullscreen mode

Slot 4 — Delta Lake + Iceberg.

RUN pip install --no-cache-dir \
    delta-spark==3.2.0 \
    pyiceberg==0.6.0
Enter fullscreen mode Exit fullscreen mode

Slot 5 — JupyterLab image for analysts.

FROM jupyter/pyspark-notebook:python-3.11

USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

USER $NB_UID

RUN mamba install -c conda-forge -y \
    duckdb=0.10.0 \
    polars=0.20.0 \
    pydantic=2.5.0 \
    pyarrow=15.0.0 \
    && mamba clean -a -y

# Custom kernels
COPY --chown=$NB_UID:$NB_GID custom-kernels /opt/kernels/
Enter fullscreen mode Exit fullscreen mode

Slot 6 — using PySpark from Airflow.

from airflow.providers.apache.spark.operators.spark_submit import SparkSubmitOperator

etl = SparkSubmitOperator(
    task_id="etl",
    application="s3://bucket/etl.py",
    conn_id="spark_default",
    executor_memory="4g",
    driver_memory="2g",
    num_executors=5,
    application_args=["--date", "2026-07-12"],
)
Enter fullscreen mode Exit fullscreen mode

Slot 7 — mamba vs pip for notebooks.

  • mamba — faster than conda; solves deps in seconds.
  • conda — original; slower on complex envs.
  • pip — fastest for pure-Python; use in slim Docker images.
  • JupyterLab image — mamba preferred for R + Python + native libs.

Slot 8 — running notebook as service.

docker run -p 8888:8888 \
    -v $PWD/notebooks:/home/jovyan/work \
    myco/jupyter:latest \
    jupyter lab --ip=0.0.0.0 --no-browser
Enter fullscreen mode Exit fullscreen mode

Slot 9 — GPU support for notebooks.

FROM jupyter/pytorch-notebook:cuda-python-3.11

RUN mamba install -c conda-forge cudatoolkit=12.1
Enter fullscreen mode Exit fullscreen mode

Requires NVIDIA container runtime on host.

Slot 10 — image size for Spark.

  • Full Spark image: ~2 GB (Java + Python + Spark libs).
  • Slim custom: ~1.2 GB via multi-stage.
  • Can't get much smaller — Spark is inherently large.

Common beginner mistakes

  • Java version mismatch — driver 17 vs cluster 11 → runtime errors.
  • Not matching Delta version to Spark version.
  • Running as root — K8s PSP rejects.
  • Missing Hadoop / cloud storage libs.
  • Notebook image without pinned kernels.

Worked example — Delta Lake ETL image

Code.

FROM apache/spark:3.5.1-python3

USER spark

RUN pip install --no-cache-dir \
    delta-spark==3.2.0 \
    pyspark==3.5.1 \
    boto3

WORKDIR /opt/spark/work-dir
COPY --chown=spark:spark etl.py /opt/spark/work-dir/

ENTRYPOINT ["spark-submit", \
    "--packages", "io.delta:delta-spark_2.12:3.2.0", \
    "--conf", "spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension", \
    "/opt/spark/work-dir/etl.py"]
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. Delta version + Spark version + Scala version must all match.

Worked example — analyst notebook stack

Code.

FROM jupyter/pyspark-notebook:python-3.11

USER $NB_UID
RUN mamba install -c conda-forge -y \
    duckdb polars pydantic sqlalchemy \
    matplotlib seaborn plotly \
    scikit-learn statsmodels \
    && mamba clean -a -y

# JupyterLab extensions
RUN pip install jupyterlab-lsp python-lsp-server

# Copy default notebooks
COPY --chown=$NB_UID:$NB_GID starter-notebooks /home/jovyan/starter-notebooks/
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. Standard analyst image contains DE + BI + ML libraries.

Worked example — Spark on K8s

Kubernetes.

apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
  name: daily-etl
spec:
  type: Python
  mode: cluster
  image: myco/spark:3.5.1
  imagePullPolicy: Always
  mainApplicationFile: local:///opt/spark/work-dir/etl.py
  sparkVersion: 3.5.1
  restartPolicy:
    type: OnFailure
    onFailureRetries: 3
  driver:
    cores: 1
    memory: 2g
    serviceAccount: spark
  executor:
    instances: 5
    cores: 2
    memory: 4g
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. SparkApplication CRD + SparkOperator = declarative Spark jobs.

dockerfile spark interview question on Java + Python + Spark version matching

A senior interviewer asks: "You have Spark 3.5.1 driver, Delta 3.2.0, and Java 17. What's the compatibility matrix?"

Solution Using explicit version pinning across all layers

FROM apache/spark:3.5.1-python3   # Java 17 baked in

USER spark
RUN pip install --no-cache-dir \
    pyspark==3.5.1 \
    delta-spark==3.2.0 \
    pandas==2.1.4

ENTRYPOINT ["spark-submit", \
    "--packages", "io.delta:delta-spark_2.12:3.2.0", \
    "--conf", "spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension"]
Enter fullscreen mode Exit fullscreen mode

Why this works — concept by concept:

  • Spark 3.5.1 requires Java 17 — base image apache/spark:3.5.1-python3 uses Java 17.
  • Delta 3.2.0 compatible with Spark 3.5.x — check compat matrix on Delta docs.
  • Scala 2.12_2.12 suffix on package coordinate matches Spark.
  • PyPI pyspark version == cluster Spark version — client-server compat.
  • Cost of mismatch — NoClassDefFoundError, ClassNotFoundException at runtime.

SQL
Topic — optimization
SQL optimization drills

Practice →

SQL Topic — SQL SQL practice library

Practice →


5. Multi-stage + best practices

The six best practices every Dockerfile ships with — .dockerignore + multi-stage + non-root + HEALTHCHECK + ENTRYPOINT exec + trivy

The mental model in one line: every production DE Docker image must combine six practices — a .dockerignore that excludes .git/.venv/__pycache__/tests, a multi-stage build with fat builder + lean runtime, a non-root user via useradd -u 1000, a HEALTHCHECK command for K8s liveness, an exec-form ENTRYPOINT ["cmd"] for SIGTERM propagation, and a trivy image scan step in CI to catch CVEs — miss any one and you pay for it in production.

Visual diagram of Docker best practices — a 6-row checklist card with ticks per practice (.dockerignore, layer cache, multi-stage, non-root, HEALTHCHECK, trivy scan) plus BuildKit chip with parallel build glyph; on a light PipeCode card.

Slot 1 — the canonical .dockerignore.

.git/
.gitignore
.gitattributes
.venv/
venv/
__pycache__/
*.pyc
*.pyo
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
node_modules/
docs/
tests/
*.log
.env
.env.*
docker-compose*.yml
.dockerignore
Dockerfile*
README.md
Enter fullscreen mode Exit fullscreen mode

Slot 2 — multi-stage builds.

FROM python:3.11-slim AS builder
# Install everything needed to compile
RUN apt-get update && apt-get install -y gcc && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.11-slim
# Copy only the installed wheels, not the compiler
COPY --from=builder /root/.local /root/.local
Enter fullscreen mode Exit fullscreen mode

Slot 3 — layer cache order.

  • Slowest-changing first. Base image, system deps, requirements.
  • Fastest-changing last. Source code.
  • Bad: COPY . . RUN pip install — cache dies on every source change.
  • Good: COPY requirements.txt . RUN pip install . COPY . . — cache lives until requirements change.

Slot 4 — non-root user.

RUN useradd -m -u 1000 appuser
USER appuser
Enter fullscreen mode Exit fullscreen mode
  • K8s PodSecurityContext often requires non-root.
  • Reduces blast radius of container escape.
  • Files copied should be chown to appuser.

Slot 5 — HEALTHCHECK.

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import myapp; print('ok')" || exit 1
Enter fullscreen mode Exit fullscreen mode
  • K8s liveness probe honours HEALTHCHECK.
  • Container marked unhealthy after 3 consecutive fails.
  • Restart triggered.

Slot 6 — ENTRYPOINT + CMD.

ENTRYPOINT ["python", "-m", "myapp"]
CMD ["--help"]
Enter fullscreen mode Exit fullscreen mode
  • Exec form ([]) — PID 1; SIGTERM propagates.
  • ENTRYPOINT = fixed executable.
  • CMD = default args (overridable via docker run image arg1).

Slot 7 — ARG vs ENV.

ARG BUILD_DATE
ARG VCS_REF
LABEL org.opencontainers.image.created=$BUILD_DATE
LABEL org.opencontainers.image.revision=$VCS_REF

ENV APP_HOME=/app
ENV LOG_LEVEL=INFO
Enter fullscreen mode Exit fullscreen mode
  • ARG — build-time variable; not available at runtime.
  • ENV — runtime environment; visible in container.
  • Use ARG for build metadata; ENV for runtime config.

Slot 8 — BuildKit for parallel + cache.

DOCKER_BUILDKIT=1 docker build \
    --cache-from myco/app:cache \
    --cache-to myco/app:cache \
    -t myco/app:1.2.3 \
    .
Enter fullscreen mode Exit fullscreen mode
  • Parallel stage execution.
  • Better cache management.
  • --mount=type=cache for pip / npm caches.

Slot 9 — trivy security scan.

trivy image --severity HIGH,CRITICAL myco/app:1.2.3
Enter fullscreen mode Exit fullscreen mode
  • Scans OS packages + language deps for CVEs.
  • CI fails if HIGH or CRITICAL found.
  • Update base images regularly.

Slot 10 — image signing with cosign.

cosign sign --key cosign.key myco/app:1.2.3
cosign verify --key cosign.pub myco/app:1.2.3
Enter fullscreen mode Exit fullscreen mode
  • Supply-chain security.
  • Kubernetes admission controller can enforce.

Slot 11 — SBOM (Software Bill of Materials).

docker buildx build --sbom=true -t myco/app:1.2.3 .
Enter fullscreen mode Exit fullscreen mode
  • Lists every package in the image.
  • Compliance requirement in some industries.

Slot 12 — tini for signal + zombie reaping.

RUN apt-get update && apt-get install -y --no-install-recommends tini && \
    rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["tini", "--", "python", "-m", "myapp"]
Enter fullscreen mode Exit fullscreen mode
  • tini reaps zombie processes.
  • Handles SIGTERM correctly.
  • Recommended for long-running processes.

Common beginner mistakes

  • No .dockerignore — image bloat + slow uploads.
  • Shell-form ENTRYPOINT — SIGTERM broken.
  • Root user — security violation.
  • Missing HEALTHCHECK — K8s can't check liveness.
  • No trivy in CI — CVEs leak to prod.
  • No SBOM — compliance gaps.
  • ARG for runtime — not available at runtime.

Worked example — the complete production Dockerfile

Code.

# syntax=docker/dockerfile:1.6
ARG PYTHON_VERSION=3.11

# ---- Builder ----
FROM python:${PYTHON_VERSION}-slim AS builder

RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --user --no-cache-dir -r requirements.txt

# ---- Runtime ----
FROM python:${PYTHON_VERSION}-slim

# Metadata
ARG BUILD_DATE
ARG VCS_REF
LABEL org.opencontainers.image.created=$BUILD_DATE
LABEL org.opencontainers.image.revision=$VCS_REF
LABEL org.opencontainers.image.source=https://github.com/myco/repo

# Non-root user
RUN useradd -m -u 1000 appuser
COPY --from=builder --chown=appuser:appuser /root/.local /home/appuser/.local
ENV PATH=/home/appuser/.local/bin:$PATH

# tini for signals + zombies
RUN apt-get update && apt-get install -y --no-install-recommends tini && \
    rm -rf /var/lib/apt/lists/*

USER appuser
WORKDIR /app
COPY --chown=appuser:appuser src /app

# Environment
ENV LOG_LEVEL=INFO PYTHONUNBUFFERED=1

# Healthcheck
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import myapp; print('ok')" || exit 1

# Signal-safe entrypoint
ENTRYPOINT ["tini", "--", "python", "-m", "myapp"]
CMD ["--help"]
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. Every DE image starts from this template.

Worked example — CI pipeline with security

GitHub Actions.

name: Docker CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          tags: myco/app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Trivy scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myco/app:${{ github.sha }}
          exit-code: 1
          severity: HIGH,CRITICAL
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. Every image push scanned; block on HIGH/CRITICAL CVEs.

Worked example — image size analysis

$ docker images myco/app
myco/app  1.2.3   ...   256MB

$ docker history myco/app:1.2.3
IMAGE          SIZE       CREATED BY
6c8f7b9a1...   150MB      /bin/sh -c useradd -m -u 1000 appuser
6c8f7b9a2...   90MB       COPY /root/.local /home/appuser/.local
6c8f7b9a3...   5MB        COPY src /app
6c8f7b9a4...   11MB       apt-get install tini

$ dive myco/app:1.2.3
# Interactive tree explorer for wasted space
Enter fullscreen mode Exit fullscreen mode

Rule of thumb. dive tool inspects layers; catches wasted space.

docker for data engineers interview question on production readiness

A senior interviewer asks: "Review this Dockerfile for prod-readiness."

FROM python:3.11
COPY . .
RUN pip install -r requirements.txt
CMD python main.py
Enter fullscreen mode Exit fullscreen mode

Solution Using the 6-property audit

Issues found:
1. FROM python:3.11 (full, ~900 MB) — should be python:3.11-slim.
2. COPY . . before requirements — cache dies on any source change.
3. Root user — security violation.
4. Shell-form CMD — SIGTERM not propagated.
5. No HEALTHCHECK — K8s can't check liveness.
6. No .dockerignore — image bloat.
7. pip install without --no-cache-dir — extra 100+ MB of caches.
8. No pinned dbt/lib versions — non-reproducible.
9. No multi-stage — dev tools in runtime.
10. No trivy scan in CI.

Fixed version:
Enter fullscreen mode Exit fullscreen mode
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.11-slim
RUN useradd -m -u 1000 appuser
COPY --from=builder --chown=appuser:appuser /root/.local /home/appuser/.local
ENV PATH=/home/appuser/.local/bin:$PATH
USER appuser
WORKDIR /app
COPY --chown=appuser:appuser . /app
HEALTHCHECK CMD python -c "import myapp" || exit 1
ENTRYPOINT ["python", "-m", "myapp"]
Enter fullscreen mode Exit fullscreen mode

Output:

Metric Original Fixed
Size 1.8 GB 250 MB
Non-root
SIGTERM safe
K8s healthcheck
Layer cache Poor Good

SQL
Topic — SQL
SQL practice library

Practice →

SQL
Topic — optimization
SQL optimization drills

Practice →


Cheat sheet — Docker recipe list

  • FROM python:3.11-slim — reasonable base for Python DE.
  • FROM apache/airflow:2.10.0-python3.11 — extend official.
  • FROM apache/spark:3.5.1-python3 — for Spark.
  • FROM jupyter/pyspark-notebook — for analyst notebooks.
  • Multi-stage builder pattern — fat builder, lean runtime.
  • RUN useradd -m -u 1000 appuser + USER appuser — non-root.
  • COPY requirements.txt . before source — layer cache order.
  • .dockerignore — exclude .git, .venv, __pycache__.
  • HEALTHCHECK CMD ... — K8s liveness.
  • ENTRYPOINT ["exec"] — signal-safe.
  • tini for zombie reap.
  • EXPOSE 8080 — documentation only.
  • ARG for build-time; ENV for runtime.
  • Pin versions everywhere.
  • Constraints file for Airflow.
  • Never bake secrets.
  • docker build --build-arg VERSION=1.2 ..
  • trivy image myco/app:1.2.3 — CVE scan.
  • cosign sign / verify — supply-chain.
  • BuildKitDOCKER_BUILDKIT=1.
  • --mount=type=cache — pip cache mount.
  • dive tool — layer size explorer.

Frequently asked questions

Why use multi-stage builds?

Multi-stage separates build dependencies (compilers, dev libraries, pip caches) from runtime. The final image is small (100-200 MB vs 800 MB+) — faster pulls, less attack surface, faster startup on K8s cold start, smaller Docker registry storage bills. The pattern: FROM ... AS builder for the fat image that installs everything; FROM ... AS runtime copies only the built artifacts. Every production Dockerfile should use it.

Should I use Alpine base images?

Only for services that don't need compiled Python packages. Alpine uses musl libc; some Python packages (like psycopg2-binary, numpy, pandas) require glibc and fail on Alpine with cryptic errors. Use python:3.11-slim (Debian slim) instead — good balance of size (150 MB) and compatibility. For truly minimal images, look at distroless (gcr.io/distroless/python3) but only after ensuring all deps work.

How do I bake in Airflow providers?

Extend the official apache/airflow:VERSION-pythonVERSION image. Use pip install --constraint URL with the constraints file matching your Airflow version + Python minor version (https://raw.githubusercontent.com/apache/airflow/constraints-2.10.0/constraints-3.11.txt). Constraints prevent provider dependency conflicts. Bake DAGs into image for stable, immutable deploys; mount from git-sync sidecar for GitOps deployments where DAGs update without image rebuild.

How do I handle secrets in Docker?

Never bake into image. Use environment variables at runtime (docker run -e SECRET=...), volume-mount secret files, or use K8s Secrets / AWS Secrets Manager / HashiCorp Vault. For Docker Compose, use .env files that aren't committed. For build-time secrets (private repo access), use BuildKit's --secret flag: --mount=type=secret,id=github_token,dst=/token. Layer history is permanent — anything in a RUN line is visible forever.

What's the difference between COPY and ADD?

Prefer COPY. ADD has extra features (auto-extract tarballs, URL fetching) that make behavior surprising and cache-unfriendly. Use COPY for files; separate RUN wget/curl for downloads. Rule — if you need to add a remote file, use RUN with wget + validation, not ADD.

How do I speed up rebuilds?

Order the Dockerfile so cache-invalidating instructions come late — dependencies before source code. Use .dockerignore to exclude bulky files (.git, .venv, node_modules). Enable BuildKit (DOCKER_BUILDKIT=1) for parallel stages and better caching. Use Docker layer cache mounts (--mount=type=cache,target=/root/.cache/pip) for pip / npm caches across builds. Use build args for common versions so cache stays warm. Typical result: source-only rebuild drops from minutes to seconds.

Practice on PipeCode

Pipecode.ai is Leetcode for Data Engineering — every `docker for data engineers` pattern above ships with hands-on practice rooms where you write a multi-stage dbt Dockerfile that drops image size from 2 GB to 250 MB, extend Airflow with providers via the constraints URL matching version + Python, ship a Spark driver image whose Java version exactly matches the cluster, run a JupyterLab image for analysts with mamba-installed Polars + DuckDB + Pydantic, and finally close the loop with `.dockerignore` + `HEALTHCHECK` + `ENTRYPOINT ["exec"]` + trivy in CI — the exact container fluency that senior DE platform interviews probe.

Practice SQL now →
Optimization drills →

Top comments (0)