Docker: Advanced Dockerfile Commands
Last updated: 2026-08-26
Production-grade Dockerfiles must not only run, but also be secure, configurable, and monitorable—all of which are achieved through advanced instructions.
1. What You'll Learn
- Principles for Choosing Between COPY and ADD
- ARG: Passing Construction Parameters
- ENV Runtime Environment Variables
- HEALTHCHECK Health Check Configuration
- Non-root Security Policy
2. A Story About a Security Audit
(1) Pain Point: Running containers as root is a high-risk vulnerability
Charlie's security audit report notes that "containers running as root" is a high-risk vulnerability. If an attacker gains access to a container through an application vulnerability, they will have root privileges and can modify system files and mount host directories. The audit score is only 45 points.
(2) Solutions for Securing Dockerfiles
Alice added a line USER appuser to the Dockerfile, raising the security score from 45 to 85.
# Security hardening: run as non-root user
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Create and switch to non-root user
RUN useradd -m appuser
USER appuser
CMD ["python", "app.py"]
(3) Benefits: Double the security score
I added just two lines (RUN useradd + USER), and the security score went from 45 to 85, passing the audit.
3. COPY vs ADD
(1) Feature Comparison
| Function | COPY |
ADD |
|---|---|---|
| Copy local files to the image | ✅ | ✅ |
| Download file from URL | ❌ | ✅ |
| Automatically extract tar/gzip/bzip2/xz | ❌ | ✅ |
Multi-stage Build --from |
✅ | ✅ |
(2) Selection Principles
COPY. The automatic extraction and URL download behavior of ADD is unpredictable and prone to errors. When extraction is needed, explicitly use the RUN tar command for greater transparency and control.
▶ Example: Differences in Behavior Between COPY and ADD (Difficulty: ⭐⭐)
# COPY: simply copies the tar file as-is
COPY archive.tar.gz /tmp/
# Result: /tmp/archive.tar.gz (still compressed)
# ADD: automatically extracts the tar file
ADD archive.tar.gz /tmp/
# Result: /tmp/archive/ (extracted contents)
▶ Example: Multi-stage build with COPY --from (Difficulty: ⭐⭐⭐)
# Multi-stage: copy artifact from builder stage
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o server
FROM alpine:3.19
COPY --from=builder /src/server /usr/local/bin/server
CMD ["server"]
- ARG and ENV
(1) Comparison of Scope of Application
| Dimension | ARG |
ENV |
|---|---|---|
| Available Stages | Build time (docker build) | Runtime (docker run) |
| Persistence | Do not write to image metadata | Write to image metadata |
| Coverable | --build-arg |
docker run -e |
| CMD/ENTRYPOINT Available | ❌ | ✅ |
| Security | Not exposed at runtime | Exposed at runtime (visible via docker inspect) |
graph TB
BUILD["Build Time"] -->|ARG| LAYER["Image Layer"]
RUN["Run Time"] -->|ENV| CONTAINER["Container Env"]
BUILD -->|ENV persists| CONTAINER
▶ Example: ARG Version Number Construction (Difficulty: ⭐⭐)
# Use ARG for build-time variables
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim
ARG APP_VERSION=1.0
LABEL version="${APP_VERSION}"
WORKDIR /app
COPY . .
CMD ["python", "app.py"]
# Build with custom Python version
docker build --build-arg PYTHON_VERSION=3.11 -t myapp:3.11 .
# Build with custom app version
docker build --build-arg APP_VERSION=2.0 -t myapp:v2.0 .
▶ Example: ENV Environment Variable Configuration (Difficulty: ⭐⭐)
# ENV: available at runtime
FROM python:3.12-slim
ENV APP_PORT=5000 \
APP_HOST=0.0.0.0 \
LOG_LEVEL=info
WORKDIR /app
COPY . .
CMD ["python", "app.py"]
# Override ENV at runtime
docker run -d -e APP_PORT=8080 -e LOG_LEVEL=debug myapp:1.0
docker inspect. Inject sensitive information at runtime using docker run -e, or use Docker Secrets.
4. WORKDIR Working Directory
▶ Example: WORKDIR—Setting the Working Directory (Difficulty: ⭐)
# WORKDIR sets the working directory for subsequent instructions
FROM python:3.12-slim
# Each WORKDIR creates the directory if it doesn't exist
WORKDIR /app
COPY . .
# WORKDIR can be relative (relative to previous WORKDIR)
WORKDIR src
RUN pwd # Output: /app/src
| Rule | Description |
|---|---|
| Use absolute paths | WORKDIR /app instead of WORKDIR app |
| Create automatically | Create automatically if the directory does not exist |
| Affects RUN/CMD/ENTRYPOINT/COPY | Subsequent commands are executed based on WORKDIR |
Do not use RUN cd |
RUN cd /app && ... is only valid for the current RUN; WORKDIR remains in effect |
5. Security Principles for Non-root Users
(1) Risks of Running Containers as root
| Risk | Description |
|---|---|
| Privilege Escalation | Container vulnerability + root privileges = Attacker gains root privileges on the host |
| File Modification | Any file within the container (including system files) can be modified |
| Mounting Risks | When mounting a host directory, the root container can modify host files |
| CIS Benchmark | "Containers run as non-root" is a mandatory requirement in the CIS Docker Security Baseline |
▶ Example: Running as a non-root USER (Difficulty: ⭐⭐)
# Best practice: create a dedicated user and switch to it
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Create non-root user with specific UID/GID
RUN groupadd -r appgroup && \
useradd -r -g appgroup -u 1000 -m appuser
# Ensure the app user owns the application files
RUN chown -R appuser:appgroup /app
# Switch to non-root user
USER appuser
CMD ["python", "app.py"]
USER appuser are executed as appuser. Even if an attacker gains access to the container, they will not have root privileges.
6. EXPOSE Port Declaration
▶ Example: EXPOSE Port Declaration (Difficulty: ⭐)
# EXPOSE documents which ports the container listens on
FROM python:3.12-slim
WORKDIR /app
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
docker run -p 5000:5000. The purpose of EXPOSE is: ① Documentation; ② docker run -P automatically selects ports declared with EXPOSE when performing random mapping.
7. HEALTHCHECK Health Check
HEALTHCHECK allows Docker to automatically check whether applications inside containers are healthy and to automatically restart them if they are not.
(1) Health Check Syntax
HEALTHCHECK [OPTIONS] CMD command
| Option | Default | Description |
|---|---|---|
--interval |
30s | Check Interval |
--timeout |
30s | Timeout |
--start-period |
0s | Container startup grace period |
--retries |
3 | Mark as "unhealthy" after X consecutive failures |
▶ Example: HEALTHCHECK curl check (Difficulty: ⭐⭐)
# Health check with curl
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
CMD ["python", "app.py"]
# Check health status
docker inspect myapp --format='{{.State.Health.Status}}'
healthy
(1) Health Status Description
| Status | Meaning | Docker Behavior |
|---|---|---|
starting |
During the grace period (start-period) | Not counted |
healthy |
Passed inspection | Operating normally |
unhealthy |
Failed retries in a row | Triggers an alert; can automatically restart when combined with a restart policy |
8. LABEL Metadata
▶ Example: Adding Metadata to LABEL (Difficulty: ⭐)
# LABEL adds metadata to the image
LABEL maintainer="alice@example.com"
LABEL version="1.0"
LABEL description="Flask web application for production"
LABEL org.opencontainers.image.source="https://github.com/example/app"
9. Complete Example: Writing a Secure Dockerfile for a Spring Boot Application
# ============================================
# Production-grade Dockerfile for Spring Boot
# Features: non-root user, HEALTHCHECK, ARG, LABEL
# ============================================
# Build argument for JAR file name
ARG JAR_FILE=app.jar
# Stage 1: Build (future multi-stage, here just copy)
FROM eclipse-temurin:21-jre-alpine
# Metadata
LABEL maintainer="dev-team@example.com"
LABEL version="2.0"
# Create non-root user
RUN addgroup -S appgroup && \
adduser -S appuser -G appgroup
# Set working directory
WORKDIR /app
# Copy the JAR file (use ARG for flexibility)
COPY target/${JAR_FILE} app.jar
# Ensure app user owns the files
RUN chown -R appuser:appgroup /app
# Switch to non-root user
USER appuser
# Expose application port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
# Run the application
ENTRYPOINT ["java", "-jar", "app.jar"]
CMD ["--server.port=8080"]
# Build with custom JAR name
docker build --build-arg JAR_FILE=myapp-2.0.jar -t spring-app:2.0 .
# Run with environment override
docker run -d -p 8080:8080 --name spring spring-app:2.0
# Verify health status
docker inspect spring --format='User: {{.Config.User}}, Health: {{.State.Health.Status}}'
User: appuser, Health: healthy
❓ FAQ
RUN tar -xzf file.tar.gz when you need to extract, to ensure transparent and controllable behavior.-e to store passwords, not ENV.--start-period to allow time for the application to start up (at least 30 seconds for Java applications); ② Check the application’s actual health endpoint (e.g., /health); don’t just verify that the port is accessible; ③ Ensure --timeout does not exceed half of --interval to avoid a backlog of checks.EXPOSE actually expose ports?EXPOSE is merely a documentation declaration that tells users which ports the container intends to use. Actual mapping requires docker run -p or -P. -P (in uppercase) automatically maps the ports declared by EXPOSE to random ports on the host.docker exec <container> whoami should return a username other than root. Or use docker inspect <container> --format='{{.Config.User}}' to view the configured user.📖 Summary
- COPY vs. ADD: Use only COPY—its behavior is transparent and controllable; ADD’s automatic decompression is an unpredictable “trap.”
- ARG: Build-time variables (not written to the image); ENV: Runtime variables (written to the image)
- WORKDIR sets the working directory; it is more reliable and persistent than
RUN cd - Switching to a non-root user is a security best practice—it only takes two lines of code
- HEALTHCHECK enables Docker to automatically monitor the health of applications and, in conjunction with a restart policy, achieve self-healing
- EXPOSE is a port declaration; the actual mapping requires the -p parameter
📝 Exercises
- Basic Exercise (Difficulty ⭐): Add a non-root user to the Dockerfile in Lesson 7. After building the image, use
docker execto verify thatwhoamidoes not output "root." - Advanced Exercise (Difficulty ⭐⭐): Add the HEALTHCHECK directive and use curl to check the application’s health endpoint. After building and running the application, use
docker inspectto view Health.Status. - Challenge (Difficulty: ⭐⭐⭐): Write a complete, production-grade Dockerfile for the Flask application from Lesson 7, including: ARG version number + ENV configuration + non-root user + HEALTHCHECK + LABEL. Then, use
docker scout quickviewortrivy imageto scan the image for security vulnerabilities.