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.
DOCKERFILE
# 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
📌 Key Point: The best practice is to use only 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: ⭐⭐)
DOCKERFILE
# 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: ⭐⭐⭐)
DOCKERFILE
# 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"]
# 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"]
BASH
# Override ENV at runtime
docker run -d -e APP_PORT=8080 -e LOG_LEVEL=debug myapp:1.0
⚠️ Note: Do not store sensitive information (passwords/keys) in ENV variables, as they will be written to the image metadata and can be viewed by anyone docker inspect. Inject sensitive information at runtime using docker run -e, or use Docker Secrets.
5. WORKDIR Working Directory
▶ Example: WORKDIR—Setting the Working Directory (Difficulty: ⭐)
DOCKERFILE
# 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
6. 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: ⭐⭐)
DOCKERFILE
# 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"]
🔒 Security: All commands (RUN/CMD/ENTRYPOINT) following USER appuser are executed as appuser. Even if an attacker gains access to the container, they will not have root privileges.
7. EXPOSE Port Declaration
▶ Example: EXPOSE Port Declaration (Difficulty: ⭐)
DOCKERFILE
# EXPOSE documents which ports the container listens on
FROM python:3.12-slim
WORKDIR /app
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
⚠️ Note: EXPOSE is a documentation declaration; it does not actually expose the port. Container port mapping still requires docker run -p 5000:5000. The purpose of EXPOSE is: ① Documentation; ② docker run -P automatically selects ports declared with EXPOSE when performing random mapping.
8. HEALTHCHECK Health Check
HEALTHCHECK allows Docker to automatically check whether applications inside containers are healthy and to automatically restart them if they are not.
# Check health status
docker inspect myapp --format='{{.State.Health.Status}}'
💻 Output:
TEXT
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
9. LABEL Metadata
▶ Example: Adding Metadata to LABEL (Difficulty: ⭐)
DOCKERFILE
# 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"
10. Complete Example: Writing a Secure Dockerfile for a Spring Boot Application
DOCKERFILE
# ============================================
# 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"]
BASH
# 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}}'
💻 Output:
TEXT
User: appuser, Health: healthy
❓ FAQ
Q Can ADD automatically extract tar archives?
A Yes, but this is a "trap feature"—if you just want to copy a tar file (without extracting it), ADD will unexpectedly extract it. Best practice: Use COPY exclusively, and explicitly use RUN tar -xzf file.tar.gz when you need to extract, to ensure transparent and controllable behavior.
Q What is the difference between ARG and ENV?
A ARG is only available during the build process and is not written to the image; ENV is written to the image and is also visible at runtime. A simple rule of thumb: use ENV for anything that needs to be accessible at runtime (such as port configuration), and use ARG for anything used only during the build (such as version numbers or download URLs). Use the runtime -e to store passwords, not ENV.
Q How should I configure HEALTHCHECK to avoid false positives?
A Three key points: ① Set --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.
Q Why shouldn’t containers be run as root?
A When a container runs as root, an attacker who gains access to the container through an application vulnerability will obtain root privileges. Combined with a kernel vulnerability, this could lead to an escape to the host. Running as a non-root user significantly reduces the attack surface—the CIS Docker Benchmark lists “run as non-root” as a mandatory requirement.
Q Does EXPOSE actually expose ports?
A No. 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.
Q How can I verify that the container is running as a non-root user?
Adocker 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 exec to verify that whoami does 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 inspect to 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 quickview or trivy image to scan the image for security vulnerabilities.
Web-Tutorial Tech Team
A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.