DEV Community

Russell Jones
Russell Jones

Posted on • Originally published at jonesrussell.github.io on

Docker from Scratch: Advanced Dockerfile Patterns

Ahnii!

Prerequisites: Docker installed, basic terminal knowledge. Recommended: Read the previous parts in this series: Part 1: Fundamentals, Part 2: Multi-Stage Builds, Part 3: Security, Part 4: Build Performance.

This is the final post in the Docker from Scratch series. You’ve covered the fundamentals, multi-stage builds, security, and performance. This post covers patterns you’ll reach for as your Dockerfiles mature: conditional logic with ARG, health checks, cross-platform builds, metadata with LABEL, and linting with hadolint.

Conditional Builds With ARG

ARG defines variables that are available during the build. Combined with shell logic, they let you create Dockerfiles that adapt to different environments.

Switch Base Image by Build Argument

ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-slim
WORKDIR /app
COPY . .
CMD ["python", "app.py"]


# Default: Python 3.13
docker build -t myapp .

# Override: Python 3.12
docker build --build-arg PYTHON_VERSION=3.12 -t myapp .

Enter fullscreen mode Exit fullscreen mode

The ARG before FROM is a special case. It’s the only instruction that can appear before FROM, and it’s only available for the FROM line itself. To use the value inside the build stage, redeclare it after FROM.

Install Dev Dependencies Conditionally

FROM node:22-alpine
ARG ENV=production
WORKDIR /app
COPY package*.json ./
RUN if ["$ENV" = "development"]; then \
      npm install; \
    else \
      npm install --omit=dev; \
    fi
COPY . .
CMD ["node", "index.js"]


# Production (default)
docker build -t myapp .

# Development with dev dependencies
docker build --build-arg ENV=development -t myapp .

Enter fullscreen mode Exit fullscreen mode

One Dockerfile, two behaviors. The shell conditional runs during RUN, so Docker evaluates it at build time. This is cleaner than maintaining separate Dockerfiles for dev and prod.

ARG Scope Rules

ARG VERSION=3.13
FROM python:${VERSION}-slim

# VERSION is no longer available here
ARG VERSION
# Now it's available again, with the same default
RUN echo "Python ${VERSION}"

Enter fullscreen mode Exit fullscreen mode

Arguments declared before FROM are consumed by FROM and then discarded. Redeclare them inside the stage if you need them later. Each stage has its own scope.

HEALTHCHECK: Let Docker Monitor Your App

HEALTHCHECK tells Docker how to test whether your container is still working. Without it, Docker only knows if the process is running, not if it’s actually responding to requests.

FROM python:3.13-slim
WORKDIR /app
COPY . .
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
CMD ["python", "app.py"]

Enter fullscreen mode Exit fullscreen mode

The options control timing:

  • --interval=30s checks every 30 seconds
  • --timeout=5s fails the check if it takes longer than 5 seconds
  • --start-period=10s gives the app 10 seconds to start before health checks count
  • --retries=3 marks the container unhealthy after 3 consecutive failures

Check container health with:

docker inspect --format='{{.State.Health.Status}}' container_name

Enter fullscreen mode Exit fullscreen mode

The status is starting, healthy, or unhealthy. Orchestration tools like Docker Swarm use this to restart unhealthy containers automatically.

Health Check for Non-HTTP Apps

Not every app has an HTTP endpoint. Use whatever makes sense for your application:

# Check if a Go binary responds
HEALTHCHECK CMD ["./server", "--health"]

# Check if a file exists (worker that writes a heartbeat)
HEALTHCHECK CMD ["test", "-f", "/tmp/worker-heartbeat"]

# Check a TCP port with netcat
HEALTHCHECK CMD ["nc", "-z", "localhost", "5432"]

Enter fullscreen mode Exit fullscreen mode

The check just needs to return exit code 0 for healthy or 1 for unhealthy.

Cross-Platform Builds With –platform

Docker images are architecture-specific. An image built on an x86 machine won’t run on ARM (like Apple Silicon Macs or AWS Graviton) without emulation. docker buildx solves this by building for multiple platforms in one command.

FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY . .
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o server ./cmd/server

FROM alpine:3.21
COPY --from=build /app/server /usr/local/bin/server
CMD ["server"]


docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

Enter fullscreen mode Exit fullscreen mode

$BUILDPLATFORM is the machine doing the build. $TARGETOS and $TARGETARCH are the platform you’re building for. Docker passes these automatically when you use --platform.

Go makes this easy because it cross-compiles natively. For interpreted languages like Python or Node.js, you don’t need the GOOS/GOARCH trick. The base image handles the architecture, so a standard Dockerfile works across platforms.

Push Multi-Platform Images

docker buildx build \
    --platform linux/amd64,linux/arm64 \
    --tag myuser/myapp:latest \
    --push .

Enter fullscreen mode Exit fullscreen mode

The --push flag sends all platform variants to the registry as a single manifest. When someone pulls myuser/myapp:latest, Docker automatically selects the right architecture.

LABEL: Add Metadata to Your Images

LABEL attaches key-value metadata to your image. It costs nothing at runtime and makes images easier to manage.

FROM python:3.13-slim
LABEL org.opencontainers.image.title="My App" \
      org.opencontainers.image.version="1.2.0" \
      org.opencontainers.image.source="https://github.com/user/myapp" \
      org.opencontainers.image.description="A Python web service"

Enter fullscreen mode Exit fullscreen mode

The org.opencontainers.image.* prefix is the OCI standard for image labels. Using standard keys means tools like container registries can display your metadata automatically.

Query labels on any image:

docker inspect --format='{{json .Config.Labels}}' myapp | jq

Enter fullscreen mode Exit fullscreen mode

SHELL: Change the Default Shell

By default, RUN instructions execute with /bin/sh -c. The SHELL instruction changes that:

FROM mcr.microsoft.com/windows/servercore:ltsc2022
SHELL ["powershell", "-Command"]
RUN Get-ChildItem C:\

Enter fullscreen mode Exit fullscreen mode

On Linux, you might switch to bash for more reliable scripting:

SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN curl -fsSL https://example.com/install.sh | bash

Enter fullscreen mode Exit fullscreen mode

The -o pipefail flag makes piped commands fail if any part of the pipeline fails. Without it, only the exit code of the last command matters, and a failed curl would be silently ignored.

Lint Your Dockerfiles With Hadolint

Hadolint is a Dockerfile linter that catches common mistakes and suggests improvements. It checks against best practices and runs ShellCheck on your RUN instructions.

# Run with Docker (no install needed)
docker run --rm -i hadolint/hadolint < Dockerfile

Enter fullscreen mode Exit fullscreen mode

Example output:

DL3008 warning: Pin versions in apt-get install
DL3059 info: Multiple consecutive RUN instructions. Consider consolidation.
SC2086 info: Double quote to prevent globbing and word splitting.

Enter fullscreen mode Exit fullscreen mode

Each rule has a code you can look up for details. Add hadolint to your CI pipeline to catch issues before they reach production.

Suppress Rules When Needed

Some rules don’t apply in every context. Suppress them with inline comments:

# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y curl

Enter fullscreen mode Exit fullscreen mode

Or create a .hadolint.yaml in your project root:

ignored:
  - DL3008

Enter fullscreen mode Exit fullscreen mode

Suppress sparingly. Most hadolint rules exist for good reasons.

ONBUILD: Instructions for Downstream Images

ONBUILD defers an instruction until someone uses your image as a base. This is useful for creating reusable base images:

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

Enter fullscreen mode Exit fullscreen mode

When someone writes FROM your-base-image, the ONBUILD instructions execute automatically. They don’t need to know about requirements.txt handling. It’s already baked into the base.

Use ONBUILD sparingly. It hides behavior, which makes debugging harder. It works well for standardized base images within a team. It’s a poor choice for public images where users expect full control.

Series Recap

Over five posts, you’ve built up from a four-line Dockerfile to production patterns:

Part Topic Key Takeaway
1 Fundamentals FROM, COPY, RUN, CMD, and layer ordering
2 Multi-Stage Builds Separate build from runtime to shrink images
3 Security & Users Non-root users, minimal bases, secrets handling
4 Build Performance Cache mounts, parallel stages, .dockerignore
5 Advanced Patterns ARG, HEALTHCHECK, cross-platform, linting

Every pattern in this series stays within the Dockerfile itself. No Compose, no orchestration, no CI/CD. Master these and you have a solid foundation for whatever comes next.

Baamaapii


Want the complete guide? All 5 parts of Docker from Scratch as a formatted ebook, plus a Dockerfile cheat sheet and 3 production-ready templates (Node.js, Python, Go). Grab the bundle on Gumroad →

Top comments (0)