ARG, ENV, HEALTHCHECK and USER

Intermediate
12 min

ARG, ENV, HEALTHCHECK and USER

Four instructions decide how configurable, observable and safe an image is. ARG and ENV both look like variables but live in different phases of the container's life. HEALTHCHECK tells Docker whether the application is actually serving, not merely running. USER removes the most common security weakness in Dockerfiles: running as root. After this lesson you will use all four correctly.

ARG: Build-Time Variables

ARG declares a variable that exists only while the image is being built. Supply a value with --build-arg, or rely on the default:

dockerfile
ARG NODE_VERSION=22 # before FROM: usable in the FROM line only FROM node:${NODE_VERSION}-alpine ARG NODE_VERSION # redeclare to use it inside the stage ARG APP_VERSION=dev RUN echo "Building $APP_VERSION on Node $NODE_VERSION"
bash
docker build --build-arg APP_VERSION=1.4.2 -t my-app:1.4.2 .

Key rules:

  • An ARG before the first FROM is global to FROM lines but must be redeclared inside a stage to be used there.
  • The value is not present in the running container; docker run my-app env will not show APP_VERSION.
  • Values are recorded in the image history, so docker history can reveal them. Never pass secrets through ARG; the secrets lesson shows the right mechanism.
  • BuildKit provides automatic args such as TARGETARCH, TARGETPLATFORM and BUILDPLATFORM, useful for downloading the right binary in multi-platform builds.

ENV: Run-Time Variables

ENV sets an environment variable in the image. It is available to every later build instruction and to the running container, where docker run -e can override it:

dockerfile
ENV NODE_ENV=production \ PORT=3000 \ PATH="/app/node_modules/.bin:${PATH}"
bash
docker run --rm my-app env | grep PORT # PORT=3000 docker run --rm -e PORT=8080 my-app env | grep PORT # PORT=8080

| | ARG | ENV | |---|---|---| | Set with | --build-arg or default | Dockerfile, -e, --env-file, Compose environment: | | Available during build | Yes | Yes (after the instruction) | | Available in the container | No | Yes | | Overridable at run time | No | Yes | | Shows in docker history | Yes | Yes |

A common pattern combines them: ARG receives a value at build time and ENV persists it, as in ARG APP_VERSION followed by ENV APP_VERSION=$APP_VERSION. Use the KEY=value form; the older ENV KEY value syntax without = is deprecated.

HEALTHCHECK: Telling Docker the App Is Ready

A container's process can be running while the application inside is deadlocked, still starting, or unable to reach its database. HEALTHCHECK defines a command Docker runs periodically; exit code 0 means healthy, 1 means unhealthy.

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD curl -f http://localhost:3000/health || exit 1

| Option | Default | Meaning | |--------|---------|---------| | --interval | 30s | Time between checks | | --timeout | 30s | Fail the check if it runs longer | | --start-period | 0s | Grace period during startup; failures do not count | | --retries | 3 | Consecutive failures before unhealthy |

The status appears in docker ps as (healthy) or (unhealthy) and in docker inspect --format '{{.State.Health.Status}}'. Compose uses it for depends_on: condition: service_healthy, and Swarm replaces unhealthy tasks. The plain Engine does not restart an unhealthy container by itself.

The check command must exist in the image: curl is absent from many slim images, so use wget -qO- or a one-liner in the application's own runtime (node -e, python -c). HEALTHCHECK NONE disables a check inherited from the base image.

USER: Dropping Root

By default every instruction and the container's process run as root. If an attacker exploits the application, they are root inside the container, and any kernel or misconfiguration issue then puts the host at risk. Create a dedicated user and switch to it before CMD:

dockerfile
FROM python:3.13-slim RUN groupadd --gid 1001 app && useradd --uid 1001 --gid app --create-home app WORKDIR /app COPY --chown=app:app . . RUN pip install --no-cache-dir -r requirements.txt USER app CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]

Practical notes:

  • Do installation steps as root first, then switch; USER applies to everything after it, including RUN.
  • Use COPY --chown so the application can read (and, where needed, write) its own files.
  • Non-root processes cannot bind ports below 1024 by default; listen on 3000 or 8080 and map -p 80:8080 on the host.
  • Many official images ship a ready user: node in the Node images, nonroot in distroless images; docker run --user 1001:1001 overrides USER at run time.
Quick Quiz
Question 1 of 3

Which variable is available inside the running container?

Key Takeaways

  • ARG is build-time only and set with --build-arg; ENV persists into the image and can be overridden with -e.
  • Neither instruction is safe for secrets: both appear in docker history.
  • HEALTHCHECK reports application readiness; Compose and orchestrators rely on it, and the check tool must exist in the image.
  • Set --start-period so slow-starting apps are not marked unhealthy during boot.
  • Install as root, then USER app; combine with COPY --chown and ports above 1024.

Next lesson: Building and Tagging Images — turn the Dockerfile into versioned images with docker build and docker tag.

ARG, ENV, HEALTHCHECK and USER - Docker | CodeYourCraft | CodeYourCraft