🐳 Section 14 · Question #4

What are the Main Instructions Used in Dockerfile

Instructions in a Dockerfile are domain-specific language (DSL) commands executed sequentially by the Docker engine / BuildKit to produce immutable image layers and metadata.

🟢 Junior Level

30-Second Interview Answer

Instructions in a Dockerfile are domain-specific language (DSL) commands executed sequentially by the Docker engine / BuildKit to produce immutable image layers and metadata.

All primary instructions fall into four functional categories:

  1. Environment and Context: FROM (defines base OS/runtime), WORKDIR (sets working directory), ENV (runtime environment variables), ARG (build-time variables).
  2. Filesystem Operations: COPY (transfers local files), ADD (copies files with auto-extraction of tar archives).
  3. Execution Commands: RUN (executes during image build to create layers), ENTRYPOINT and CMD (execute at container startup).
  4. Configuration and Security: USER (switches to an unprivileged UID), EXPOSE (documents network ports), VOLUME (declares external mount points), HEALTHCHECK (monitors container liveness).

Clean Java 21 / Spring Boot 3 Example

# 1. Base runtime image
FROM eclipse-temurin:21-jre-jammy

# 2. Establish working directory inside container (auto-created if missing)
WORKDIR /app

# 3. Inject JVM ergonomics for container cgroups
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"

# 4. Copy pre-compiled JAR artifact
COPY target/service.jar app.jar

# 5. Create and switch to non-root user for security
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser

# 6. Document application port (Metadata only)
EXPOSE 8080

# 7. Fixed container entrypoint in Exec Form
ENTRYPOINT ["java", "-jar", "app.jar"]

🟡 Middle Level

Key Instruction Deep Dive

1. RUN vs CMD vs ENTRYPOINT

  • RUN: Executed once during image creation (docker build). Installs OS packages (apt-get), compiles code, creates users, and commits a permanent read-only layer to the image.
  • ENTRYPOINT: Defines the fixed executable binary launched whenever the container starts (docker run). Overriding it requires an explicit --entrypoint CLI flag.
  • CMD: Supplies default arguments to the ENTRYPOINT (or defines a default command if no ENTRYPOINT is present). It is easily overridden by passing arguments at the end of docker run.

2. COPY vs ADD

  • COPY: Transparent, safe, and recommended for 95% of use cases. Directly transfers files or directories from the local build context into the image.
  • ADD: An extended copy command with extra automation:
    1. Automatically extracts compressed local tarballs (.tar, .tar.gz, .tar.bz2) into the target directory.
    2. Can fetch files from remote URLs (not recommended for production because it cannot clean up downloaded archive files within the same layer).

3. ARG vs ENV

| Feature | ARG (Build Argument) | ENV (Environment Variable) | |—|—|—| | Lifecycle | Available only during build (docker build) | Available both during build and in running container | | Override Mechanism | Passed via --build-arg KEY=value | Passed via -e KEY=value or --env-file | | Security Implication | Baked in plaintext in docker history (Do NOT use for secrets!) | Visible via docker inspect and to all container processes | | Common Use Case | Specifying package versions, compiler flags | Database URLs, active Spring profiles, heap tuning |

4. EXPOSE and VOLUME

  • EXPOSE 8080: Acts purely as documentation between the image author and system operators. It does not publish or bind the port to the host interface. To expose traffic outside the Docker host, you must explicitly bind ports at runtime using docker run -p 8080:8080.
  • VOLUME ["/data"]: Declares a mount point to decouple persistent or heavy I/O data from the ephemeral container Overlay2 filesystem. Writes to this path bypass the container’s copy-on-write layer.

🔴 Senior Level

Atomic Layer Chaining in RUN

Every distinct RUN instruction commits an immutable Overlay2 layer. Cleaning temporary caches in a subsequent RUN step creates a whiteout marker but permanently traps downloaded bytes in parent layers:

# ❌ ANTI-PATTERN: Traps 150 MB of apt cache files in intermediate layer!
RUN apt-get update
RUN apt-get install -y --no-install-recommends curl jq
RUN rm -rf /var/lib/apt/lists/*

# ✅ PRODUCTION PATTERN: Atomic single layer with zero leftover build debris
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl jq && \
    rm -rf /var/lib/apt/lists/*

Exec Form vs Shell Form & The PID 1 Graceful Shutdown Problem

Any process-starting directive (RUN, CMD, ENTRYPOINT) can be written in two distinct syntactic notations:

+-------------------------------------------------------------------+
|               Exec Form vs Shell Form Process Tree                |
|                                                                   |
|   1. Exec Form: ENTRYPOINT ["java", "-jar", "app.jar"]            |
|      Host Kernel                                                  |
|      └── Container PID 1: java (JVM)                              |
|          * Directly receives SIGTERM from Docker/Kubernetes       |
|          * Executes Spring Boot shutdown hooks & flushes buffers  |
|                                                                   |
|   2. Shell Form: ENTRYPOINT java -jar app.jar                     |
|      Host Kernel                                                  |
|      └── Container PID 1: /bin/sh -c "java -jar app.jar"          |
|          └── Container PID 2: java (JVM)                          |
|          * /bin/sh drops SIGTERM; JVM never receives signal       |
|          * Killed after grace period by SIGKILL (Exit Code 137)   |
+-------------------------------------------------------------------+
  • Shell Form (ENTRYPOINT command param): Docker implicitly invokes /bin/sh -c "command param". The shell process becomes PID 1. Standard POSIX shells do not forward signals (SIGTERM) to child processes. When Kubernetes stops the container, Java never receives SIGTERM. Shutdown hooks, active database transaction rollbacks, and Kafka commit offsets are abandoned. After 30 seconds, the kernel issues SIGKILL (Exit 137).
  • Exec Form (ENTRYPOINT ["executable", "param1", "param2"]): Docker calls execve(2) directly without spawning a subshell. The JVM process becomes PID 1, intercepting SIGTERM gracefully.

The VOLUME Declaration Ordering Trap

A critical edge case that trips up many engineers is declaring VOLUME before configuring directory ownership or initial files:

  • Once an instruction VOLUME ["/app/data"] is encountered, Docker creates a temporary anonymous volume mount for that path during all subsequent build steps.
  • If a later step runs RUN chown -R appuser:appgroup /app/data or RUN touch /app/data/init.db, those mutations are written into the temporary build-time volume.
  • When the build step finishes, the temporary volume is discarded. The changes are never saved into the final image layers!
  • At runtime, /app/data will revert to root:root ownership, causing the non-root application to fail with Permission denied.
  • Golden Rule: Always create directories, assign file ownership with chown, and seed initial data before declaring the VOLUME instruction.

Container Orchestration Health Checks (HEALTHCHECK)

HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
  • Instructs the Docker Engine to actively monitor application health.
  • Containers transition from starting to healthy or unhealthy.
  • --start-period=60s: Crucial for enterprise Java/Spring Boot applications to prevent false-positive kills during JIT compilation and cold JVM boot phases.

4 Tricky Questions with In-Depth Answers

1. What hidden danger occurs if RUN chown -R appuser /app/data is placed AFTER VOLUME ["/app/data"] in a Dockerfile?

Answer: The VOLUME instruction explicitly tells Docker that the specified directory is an external mount point rather than part of the union filesystem. For every subsequent instruction during the build, Docker mounts a temporary anonymous volume over /app/data. The RUN chown -R appuser /app/data command changes ownership inside that temporary volume. Once the step finishes, Docker commits the container layer diff—which contains nothing because the changes were written to the volume—and discards the volume. As a result, in the final image, /app/data remains owned by root. When the container is launched with USER appuser, the process crashes with java.io.IOException: Permission denied when attempting to write to /app/data.

2. What is the difference in variable scope between ARG declared BEFORE the first FROM instruction vs ARG declared AFTER FROM?

Answer:

  • An ARG declared before the initial FROM line lives in the global build scope. Its only purpose is to parameterize FROM statements (for example, choosing a base image version: ARG JAVA_VER=21 $\to$ FROM eclipse-temurin:${JAVA_VER}).
  • Once FROM executes, Docker enters an isolated build stage. The global ARG goes out of scope and becomes completely inaccessible to subsequent RUN, ENV, or COPY instructions.
  • To use that variable inside a build stage, you must re-declare the ARG inside that stage without specifying a default value:
    ARG JAVA_VER=21
    FROM eclipse-temurin:${JAVA_VER}
    ARG JAVA_VER # Re-declared to bring into the stage scope
    RUN echo "Compiling on Java $JAVA_VER"
    

3. How do ENTRYPOINT and CMD interact when both are declared in Exec Form? Show an example with Spring Boot profiles and ports.

Answer: When both are declared in Exec Form (JSON array), ENTRYPOINT defines the executable binary and its immutable parameters, while CMD appends default, overridable arguments:

ENTRYPOINT ["java", "-jar", "app.jar"]
CMD ["--server.port=8080", "--spring.profiles.active=prod"]
  • Running docker run order-service executes: java -jar app.jar --server.port=8080 --spring.profiles.active=prod.
  • Running docker run order-service --server.port=9090 overrides CMD entirely, executing: java -jar app.jar --server.port=9090. This allows image creators to enforce a strict binary entrypoint while allowing operators to pass dynamic CLI arguments effortlessly.

4. Why is RUN curl -fSL ... | tar -xz && rm preferred over ADD http://... for fetching remote archives?

Answer:

  1. No Auto-Extraction for URLs: ADD only automatically unpacks .tar.gz files from the local build context. When pointed to an HTTP URL, it simply downloads the raw archive without extracting it.
  2. Layer Bloat: ADD writes the downloaded archive into an immutable layer. Even if you run RUN rm archive.tar.gz in the next instruction, the compressed archive remains permanently inside the image history.
  3. Integrity & Verification: ADD lacks support for HTTP headers, basic authentication, retry logic, and cryptographic SHA-256 checksum verification. Using RUN curl ... | tar -xz streams the archive directly into the tar extraction utility and cleans up in a single execution step, leaving no lingering archive files in the layer history.

🎯 Interview Cheat Sheet

Summary of Core Dockerfile Instructions

| Instruction | Phase | Primary Purpose | Best Practice / Pro-Tip | |—|—|—|—| | FROM | Build | Sets base OS and runtime | Always pin specific version tags; avoid :latest | | WORKDIR | Build/Run | Sets working directory | Replaces cd; creates directories automatically | | RUN | Build | Executes commands to create layers | Chain with && \ and clean caches in the same command | | COPY | Build | Copies files from build context | Always prefer over ADD; use --chown flag | | ADD | Build | Copies with auto-tar extraction | Use only for local .tar.gz unpacking | | ARG | Build | Build-time parameterization | Scoped per stage; never use for passwords | | ENV | Build/Run | Injects runtime environment vars | Persists into container runtime environment | | USER | Run | Sets non-root execution UID | Enforce least-privilege security (USER 10001) | | EXPOSE | Metadata | Documents container listening ports | Documentation only; does not publish ports to host | | VOLUME | Metadata/Run| Declares external mount points | Declare after directory creation and permissions setup | | ENTRYPOINT| Run | Fixed primary executable | Always use Exec Form (["java", "-jar", ...]) for PID 1 | | CMD | Run | Default overridable arguments | Appends default arguments to ENTRYPOINT | | HEALTHCHECK| Run | Declares container health probe | Set --start-period for slow-starting Java services |

Red Flags (DO NOT Say)

  • ❌ “EXPOSE 8080 opens port 8080 to the internet.” (It is purely documentary metadata; runtime binding requires docker run -p 8080:8080).
  • ❌ “RUN cd /app sets the current folder for subsequent steps.” (Each RUN executes in a fresh subshell; use WORKDIR /app).
  • ❌ “I pass passwords using ARG because args disappear in the running container.” (ARG values are permanently stored in cleartext inside docker history).
  • ❌ “Shell form ENTRYPOINT java -jar app.jar is fine in Kubernetes.” (Wraps Java in /bin/sh as PID 1, preventing graceful SIGTERM propagation and corrupting transactions upon Pod eviction).