Building Custom Docker Images with Dockerfile Syntax

A Dockerfile is a plain-text recipe that defines how to assemble a Docker image. It consists of a series of instructions executed sequentially by the docker build command. Rather than manually configuring containers, Dockerfiles enable reproducible, version-controlled, and automated image creation.

Below is a modernized Redis Dockerfile example—rewritten for clarity, security, and best practices (e.g., using a minimal base image, non-root user, multi-stage build, and explicit dependency menagement):

# Multi-stage build: compile Redis in builder stage
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache build-base wget tar
WORKDIR /tmp
RUN wget -qO- https://download.redis.io/redis-stable.tar.gz | tar xz
WORKDIR /tmp/redis-stable
RUN make -j$(nproc) && make install

# Final runtime stage
FROM alpine:3.20
RUN addgroup -g 1001 -f redis && adduser -S redis -u 1001
COPY --from=builder /usr/local/bin/redis-* /usr/local/bin/
COPY --from=builder /tmp/redis-stable/redis.conf /etc/redis.conf

# Harden configuration
RUN sed -i 's/^bind .*/bind 127.0.0.1 ::1/' /etc/redis.conf && \
    sed -i 's/^daemonize .*/daemonize no/' /etc/redis.conf && \
    sed -i 's/^dir .*/dir \/data/' /etc/redis.conf && \
    sed -i 's/^logfile .*/logfile \/dev\/stdout/' /etc/redis.conf

VOLUME ["/data"]
WORKDIR /data
EXPOSE 6379
USER redis
CMD ["redis-server", "/etc/redis.conf"]

This example demonstrates key improvements over legacy patterns: separation of build and runtime concerns, reduced attack surface via Alpine Linux, non-root execution, and safer default bindings.

Core Dockerfile Instructions

  • FROM: Declares the base image. Must be the first instruction. Supports tags and digests (e.g., FROM ubuntu:22.04 or FROM python@sha256:abc123...).
  • ARG: Defines build-time variables (e.g., ARG NODE_VERSION=20.12.0), passed via --build-arg. Not available at runtime unless copied into the image.
  • RUN: Executes commands during build. Prefer shell form for simple logic (RUN apt-get update && apt-get install -y curl) or exec form for predictability (RUN ["sh", "-c", "echo hello"]). Each RUN creates a new layer.
  • COPY and ADD: COPY is preferred for local files/directories (COPY ./src /app/src). ADD supports remote URLs and auto-extraction of tar archives—but avoid it for security and transparency reasons.
  • ENV: Sets persistent environment variables visible both during build and at runtime (ENV PATH="/app/bin:$PATH").
  • EXPOSE: Documents which ports the container listens on. Does not publish them—use -p or -P at docker run time to map host ports.
  • VOLUME: Declares mount points for persistent data. Ensures the directory is stored outside the Union File System (e.g., VOLUME ["/var/lib/mysql"]).
  • WORKDIR: Sets the working directory for subsequent instructions (WORKDIR /app affects RUN, CMD, ENTRYPOINT, etc.). Creates the path if missing.
  • USER: Switches to a non-root user before runtime (USER appuser:appgroup). Critical for security hardening.
  • ENTRYPOINT and CMD: Define the container’s default executable and arguments. ENTRYPOINT configures the executable (often as a wrapper script); CMD supplies its default arguments. They combine when both are present in exec form: ENTRYPOINT ["./entrypoint.sh"] + CMD ["--verbose"] → runs ./entrypoint.sh --verbose.
  • ONBUILD: Triggers instructions only when the current image is used as a base for another build. Rarely used in modern workflows due to maintainability concerns.
  • LABEL: Adds metadata (e.g., LABEL org.opencontainers.image.source="https://github.com/example/app") for traceability and tooling integration.

Dockerfiles should follow the principle of least privilege, minimize layers, avoid hardcoded secrets, and prioritize reproducibility—using pinned base images and explicit dependency versions.

Tags: docker dockerfile containerization alpine-linux multi-stage-build

Posted on Wed, 29 Jul 2026 16:35:44 +0000 by Mad_Mike