Skip to Main Content
Docker Learning Template: Alpine Multi-Stage, Non-Root, Compose Patterns
TUTORIAL

Docker Learning Template: Alpine Multi-Stage, Non-Root, Compose Patterns

A comprehensive tutorial on a Docker learning template using Alpine multi-stage builds, a non-root runtime user, and practical Docker Compose patterns, including inline demo app, commented PostgreSQL, and cross-platform build guidance.

Introduction In modern Docker practice, a learning template can accelerate onboarding for developers and DevOps engineers. The Dockertest project is a minimal, security-minded Docker template built around Alpine Linux, designed to teach and enforce best practices: small images via multi-stage builds, secure execution as a non-root user, and pragmatic orchestration patterns with Docker Compose. This post walks you through the template, explains the rationale behind each pattern, and provides concrete code snippets you can copy, adapt, and extend in your own projects.

What this template demonstrates at a glance

  • Base: Alpine Linux for a tiny footprint and predictable behavior.
  • Multi-stage build: base → build → final to minimize runtime image size while keeping build-time dependencies isolated.
  • Security: runs as a non-root user with UID 10001 to reduce risk surface.
  • Compose patterns: a single-service template with a commented PostgreSQL setup, health checks, secrets, and persistent volumes.
  • Ignore rules: a focused .dockerignore excludes Docker-related files, language artifacts, and IDE/OS noise to keep builds clean.
  • Documentation: README.Docker.md provides local and cross-platform build/run commands to ease hands-on practice.

Why this template matters for developers and DevOps engineers

  • It offers a repeatable, security-minded baseline for building microservices with Docker that you can evolve as you learn.
  • The Alpine base keeps images small, which translates to faster builds, transfers, and shorter attack surfaces.
  • Non-root execution is a core security pattern increasingly required by teams and regulators for containerized workloads.
  • Compose patterns give you an approachable development workflow that mirrors production concerns, including health checks, secrets, and optional databases.
  • The template is intentionally minimal. It’s a solid starting point for students and teams that want muscle memory around secure, portable containers without adding unnecessary complexity.

Project skeleton at a glance

  • Dockerfile: a three-stage pipeline (base, build, final) with an inline demo app using a heredoc and a dedicated non-root user.
  • compose.yaml: a single-service setup that targets the final stage for lean runtime, plus a commented PostgreSQL template for local experimentation.
  • .dockerignore: pragmatic exclusions to keep builds fast and clean.
  • README.Docker.md: quick-start commands for local workflows and cross-platform builds.

A closer look at the Dockerfile (multi-stage, inline demo app, non-root user) The Dockerfile in the template follows a straightforward multi-stage pattern. The key ideas are:

  • Stage 1 (base): install only what you need for runtime and define a non-root user.
  • Stage 2 (build): bring in compiler/toolchain to build a small demo app; use an inline demo app via a heredoc to keep the repository compact.
  • Stage 3 (final): copy the built app from the build stage, switch to the non-root user, and expose the runtime port.
  • The inline demo app is embedded with a heredoc to illustrate how you can prototype quickly without committing large source files.

Dockerfile (illustrative)

# Stage 1: base
FROM alpine:3.18 AS base
# Create a dedicated non-root user with UID 10001
RUN addgroup -S dockgroup && adduser -S -D -u 10001 -G dockgroup dockuser
WORKDIR /app

# Stage 2: build
FROM base AS build
# Install build tools (Go for a tiny HTTP server) and libc headers if needed
RUN apk add --no-cache go musl-dev

# Inline demo app (Go) via heredoc
RUN mkdir -p /src
COPY <<'EOF' /src/app.go
package main

import (
  "fmt"
  "net/http"
)

func main() {
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello from dockertest (Go)!)")
  })
  // Simple health endpoint for testing
  http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(200)
    fmt.Fprintln(w, "OK")
  })
  _ = http.ListenAndServe(":8080", nil)
}
EOF

# Build the binary
RUN go build -o /app/app /src/app.go

# Stage 3: final (runtime)
FROM alpine:3.18 AS final
# Re-create non-root user in the final stage for security
RUN addgroup -S dockgroup && adduser -S -D -u 10001 -G dockgroup dockuser
WORKDIR /app

# Copy the compiled app from the build stage
COPY --from=build /app/app /usr/local/bin/app

# Run as non-root for security
USER 10001

# Expose the HTTP port
EXPOSE 8080

# Entry point
CMD ["/usr/local/bin/app"]

Notes on the Dockerfile

  • The three-stage approach ensures the final image contains only the runtime artifact and essential runtime dependencies, minimizing surface area.
  • A dedicated non-root user with UID 10001 is created in both the base and final stages to guarantee the container runs under a non-privileged context.
  • The inline demo app demonstrates how you can embed a quick prototype without adding external source files to the repository. In real-world scenarios, you’d typically copy a real app source tree and build it with the appropriate language toolchain.
  • The health and readiness of the service can be validated by the Compose health checks described later in this post.

A note on the inline demo app approach Inline code via a heredoc is a concise learning pattern for templates and quick prototyping. In production templates, you’d typically bring in a proper source tree from your version control system (Git) and rely on a formal build process. The dockertest template uses the inline approach to emphasize the learning objective: a small, repeatable build that demonstrates multi-stage assembly and non-root execution without heavy scaffolding.

Docker Compose patterns: single-service focus with a runnable PostgreSQL template (commented) The compose.yaml in this template is designed for simplicity and clarity. It centers on a single service targeting the final stage of the Dockerfile, mirroring a microservice pattern. To help learners explore a practical database-backed scenario, the file includes a commented PostgreSQL template. This pattern gives you a quick way to test cross-service configurations while keeping the primary focus on the app container.

Compose.yaml (illustrative)

version: "3.9"

services:
  dockertest:
    build:
      context: .
      dockerfile: Dockerfile
      target: final
    image: dockertest:latest
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "wget", "-q", "-O-", "http://localhost:8080/health"]
      interval: 15s
      timeout: 5s
      retries: 4
    depends_on:
      postgres:
        condition: service_healthy
    secrets:
      - postgres_password
    volumes:
      - dockertest_data:/data

  # PostgreSQL example (commented for a lean initial run)
  # postgres:
  #   image: postgres:15
  #   environment:
  #     POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
  #   secrets:
  #     - postgres_password
  #   volumes:
  #     - postgres_data:/var/lib/postgresql/data
  #   healthcheck:
  #     test: ["CMD", "pg_isready", "-U", "postgres"]
  #     interval: 10s
  #     timeout: 5s
  #     retries: 5

secrets:
  postgres_password:
    file: ./secrets/postgres_password.txt

volumes:
  dockerhost_data:
  dockertest_data:
  postgres_data:

Understanding the compose pattern

  • Targeting the final stage: The build section specifies target: final, ensuring the final image used at runtime is minimal and free from build-time dependencies.
  • Health checks: The Compose healthcheck for the dockertest service allows you to gate startup logic on a healthy app, aligning with best practices for robust service orchestration.
  • Secrets: The template demonstrates integrating sensitive data via docker secrets, keeping credentials out of the image itself.
  • Volumes: A persistent volume for application data ensures that state, logs, or other data survive container restarts.
  • Optional PostgreSQL: The commented block shows how you would wire a database for a more complete integration test. This makes it easy to toggle a database pattern without changing the core app service.
  • Service interdependencies: The depends_on with a health gate ensures the app only begins startup logic after the DB (if used) is ready, which mirrors real deployments where services must coordinate startup order.

. dockerignore: what to exclude to keep builds lean A focused .dockerignore file helps keep build contexts small and predictable. The template’s ignore rules target Docker-related noise, language artifacts, and IDE/OS files that do not contribute to your runtime image.

.dockerignore (illustrative)

# Ignore Docker-related and build artifacts
Dockerfile
compose.yaml
README.Docker.md

# Language/build artifacts
node_modules/
*.log
*.tmp
*.cache/
*.pyc
__pycache__/

# IDE and OS junk
.DS_Store
*.swp
*.sublime-project
.vscode/
.idea/

# OS-specific
.DS_Store
Thumbs.db

README.Docker.md: quick-start commands for practice and cross-platform builds The README is where most learners start hands-on. It documents practical steps to use the template in local development and CI/CD pipelines. Here are the key commands the template provides:

  • Build and run with Docker Compose:

    • docker compose up --build
    • This command builds the image(s) and starts the container(s) per the compose.yaml configuration.
  • Standalone build and run (Dockerfile-based):

    • docker build -t dockertest .
    • docker run -p 8080:8080 dockertest
  • Cross-platform build (AMD64 on non-native hosts):

    • docker build --platform=linux/amd64 -t myapp .

Cross-platform considerations

  • The template highlights a cross-platform build pattern (linux/amd64) to help teams validate builds in CI/CD pipelines and cloud environments. This is particularly relevant for multi-architecture workflows where you want to ensure a consistent binary across platforms.

Case studies and practical illustrations Case Study 1: Local development loop

  • A developer runs docker compose up --build to spin up the service with the final image, validate the /health endpoint, and confirm that the Go-based HTTP server responds.
  • If a database is desired, they uncomment the postgres block, provide a secret file at secrets/postgres_password.txt, and rely on the health gate to ensure the database is ready before the app starts.
  • The health check and volume for /data provide a stable, testable loop that mirrors production concerns in a lightweight way.

Case Study 2: Security-first onboarding

  • Security-conscious teams adopt non-root execution by default. The template’s UID 10001 is a concrete, auditable choice, making it straightforward to compare against organizational security baselines.
  • Secrets usage in Compose keeps credentials external to the image, enabling safer rotation and reduced blast radius in case an image is compromised.

Case Study 3: CI/CD integration and cross-platform build

  • In a CI workflow, you can leverage docker buildx to produce multi-arch images and publish to a registry. The docker compose approach remains a practical developer workflow, while the final image remains lean by virtue of the multi-stage build.
  • The cross-platform build snippet in README.Docker.md demonstrates how to validate deployment targets across environments (local ARM64 runner, CI runner on x86_64, etc.).

Best practices learned from the template

  • Use multi-stage builds to minimize final image size and surface area. Separate build-time dependencies from runtime dependencies to keep your images lean.
  • Run containers as non-root users whenever possible. UID 10001 in the template provides a concrete, auditable baseline to compare against your security guidelines.
  • Treat Compose as a development pattern, not a production orchestration model. When you move toward production or Kubernetes, you’ll translate these patterns into manifests and workflows that fit the target platform.
  • Include health checks and secrets management early. Health checks improve reliability, while secrets keep sensitive data out of the image and out of logs.
  • Keep documentation close to the code. The README.Docker.md in this template helps developers quickly reproduce environments and validate behavior across platforms.

Limitations and what’s outside the scope

  • This learning template focuses on Dockerfile patterns and Compose-based development workflows. It intentionally does not replace a Kubernetes or AKS deployment guide. When you move to Kubernetes, you’ll translate the Compose constructs into Deployments, Services, ConfigMaps, Secrets, and appropriate health checks. The knowledge provided here can complement Kubernetes learnings, but the concrete migration patterns require a separate set of resources.
  • Alpine multi-stage patterns, nuanced non-root user strategies, and advanced Compose-to-Kubernetes migration techniques benefit from dedicated Docker and Kubernetes resources. If you’re adopting this template as a starting point for a larger project, plan to supplement with external references for deeper dockerfile best practices and Kubernetes migrations.

How this fits into a broader learning path (context from the broader knowledge base)

  • The knowledge base emphasizes deployment, security, networking, and governance patterns in containerized environments, including Azure Kubernetes Service (AKS) and related topics. While AKS-specific guidance is not the focus of this Docker template, it reinforces the importance of:

    • Security posture (RBAC, workload identity, OIDC), image hygiene, and deployment safeguards.
    • Networking patterns (CNI overlays, ingress options, egress patterns) and observability tooling (Managed Prometheus, Grafana).
    • The distinction between Kubernetes-native manifests and development workflows that use containers locally (e.g., Compose) for rapid iteration.

Putting it all together: how to use this template in your learning journey

  1. Start locally with the single-service template
  • Run docker compose up --build to verify that the app starts and responds on port 8080.
  • Validate health checks and ensure the non-root user context is active by inspecting the running container.
  1. Experiment with the PostgreSQL pattern (commented)
  • Uncomment the postgres service in compose.yaml and provide secrets to test a database-backed scenario.
  • Observe startup order and health gating via depends_on with service health checks.
  1. Try a cross-platform build
  • Use docker build --platform=linux/amd64 -t myapp . to validate cross-platform compatibility.
  1. Translate the pattern to production-grade workflows (beyond this template)
  • For production deployments, translate Compose concepts into Kubernetes manifests and leverage CI/CD pipelines to build and deploy images to your registry.
  • Consider adopting a security-first baseline across your environments, with clear governance around secrets, image scanning, and runtime user permissions.

Conclusion Docker Learning Template: Alpine Multi-Stage, Non-Root, Compose Patterns provides a compact, secure, and repeatable foundation for modern container development. By combining an Alpine base, a true multi-stage build, and a non-root execution model, you gain a lean runtime that is easier to audit and reason about. The Compose patterns offer a practical way to practice orchestration concerns, including health checks, secrets management, and optional database integration, all while keeping the learning curve approachable. Use this template as a first-step catalyst: build your muscle memory, then branch out into production-grade patterns, CI/CD automation, and Kubernetes migrations as your projects grow.

If you’d like, I can tailor the template to your language of choice (Go, Node.js, Python, etc.), adapt the inline demo app to demonstrate language-specific characteristics, or extend the Compose file with more production-oriented features like secrets encryption, config maps, or more elaborate health-check strategies.