Ravindra BagaleCourses & study guides मराठी

Chapter 52: Dockerfile Instructions Explained: FROM, RUN, CMD vs ENTRYPOINT, COPY vs ADD, Multi-Stage and BuildKit

52.7 HEALTHCHECK, VOLUME, LABEL, STOPSIGNAL, ONBUILD and SHELL

Friends, these six instructions are used less often, but each one solves a real problem. HEALTHCHECK tells Docker if the app is really working, VOLUME marks data folders, LABEL adds information, STOPSIGNAL controls shutdown, ONBUILD and SHELL change how builds behave. One by one, with examples.

1. Start from a story

Neha was on call for the booking service at MakeMyTrip. At 2 AM the dashboard said every container was Up, yet customers saw errors.

The app process was alive but stuck: its database pool was exhausted and every request hung. Docker only checks that the process exists, so it reported Up and nothing restarted.

Next morning Neha added a HEALTHCHECK that called the app's own health endpoint. She also added LABEL lines with the team name and git commit, and a STOPSIGNAL so the app finished bookings before stopping.

The next time the pool got stuck, the container turned unhealthy within a minute and the orchestrator replaced it. In this lesson we add the same lines one by one.

Every intervalDocker runs the test commandExit 0healthyExit 1, retries timesunhealthyOrchestratorreplaces the container
Picture 1: how a health check works

2. HEALTHCHECK

Step 1: Add a health check

Why: Up does not mean working.

What: HEALTHCHECK CMD runs a command inside the container. Exit 0 means healthy, 1 means unhealthy.

Example 1: Add it

Type this:

printf 'FROM nginx:1.27\nHEALTHCHECK --interval=10s --timeout=3s --retries=3 CMD curl -fsS http://localhost/ || exit 1\n' > Dockerfile
docker build -t h1 . && docker run -d --name h1 h1 && sleep 15 && docker ps --filter name=h1 --format '{{.Status}}'

You will see:

Up 15 seconds (healthy)

What it means: Status now includes health.

Example 2: Break it

Type this:

docker exec h1 rm /usr/share/nginx/html/index.html && sleep 35 && docker ps --filter name=h1 --format '{{.Status}}'

You will see:

Up 50 seconds (unhealthy)

What it means: Process alive, health check failed.

Example 3: See details

Type this:

docker inspect h1 -f '{{range .State.Health.Log}}{{.ExitCode}} {{end}}'

You will see:

0 0 22 22 22

What it means: curl exit 22 means HTTP error.

Step 2: Health check options

Why: Wrong timing gives false alarms.

What: --interval, --timeout, --retries, --start-period and --start-interval tune the check.

Example 4: Slow starter

Type this:

printf 'FROM eclipse-temurin:21-jre\nHEALTHCHECK --start-period=60s --interval=15s CMD wget -qO- localhost:8080/actuator/health || exit 1\n' > Dockerfile && tail -1 Dockerfile

You will see:

HEALTHCHECK --start-period=60s --interval=15s CMD wget -qO- localhost:8080/actuator/health || exit 1

What it means: Failures in the first 60 s do not count.

Example 5: Starting state

Type this:

docker run -d --name j1 javaapp && docker ps --filter name=j1 --format '{{.Status}}'

You will see:

Up 5 seconds (health: starting)

What it means: Grace period in action.

Example 6: Disable at run

Type this:

docker run -d --no-healthcheck --name h2 h1 && docker ps --filter name=h2 --format '{{.Status}}'

You will see:

Up 2 seconds

What it means: No health shown.

Step 3: No curl in the image

Why: Slim images often lack curl.

What: Use the app's own language, wget, or a tiny binary.

Example 7: Python

Type this:

printf 'FROM python:3.12-slim\nHEALTHCHECK CMD python -c "import urllib.request,sys; urllib.request.urlopen(\x27http://localhost:8000/health\x27)" || exit 1\n' > Dockerfile && tail -1 Dockerfile

You will see:

HEALTHCHECK CMD python -c "import urllib.request,sys; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

What it means: Python checks itself.

Example 8: Node

Type this:

printf 'FROM node:22-slim\nHEALTHCHECK CMD node -e "fetch(\x27http://localhost:3000/health\x27).then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"\n' > Dockerfile && tail -1 Dockerfile

You will see:

HEALTHCHECK CMD node -e "fetch('http://localhost:3000/health')..."

What it means: Built-in fetch in Node 22.

Example 9: Postgres

Type this:

docker run -d --name pg -e POSTGRES_PASSWORD=example-only --health-cmd 'pg_isready -U postgres' --health-interval 5s postgres:16 && sleep 10 && docker ps --filter name=pg --format '{{.Status}}'

You will see:

Up 10 seconds (healthy)

What it means: Health check from the command line.

3. VOLUME, LABEL and STOPSIGNAL

Step 4: VOLUME marks a data folder

Why: Data written there should outlive the container.

What: VOLUME /path makes Docker create an anonymous volume there if you do not mount one.

Example 10: Declare

Type this:

printf 'FROM alpine:3.20\nVOLUME /data\nCMD ["sh","-c","date > /data/t; sleep 60"]\n' > Dockerfile
docker build -t v1 . && docker run -d --name v1 v1 && docker inspect v1 -f '{{range .Mounts}}{{.Type}} {{.Destination}}{{end}}'

You will see:

volume /data

What it means: Anonymous volume created.

Example 11: Hidden volumes pile up

Type this:

docker volume ls -qf dangling=true | wc -l

You will see:

14

What it means: Every run made a new one.

Example 12: Name it instead

Type this:

docker run -d --name v2 -v appdata:/data v1 && docker volume ls --filter name=appdata

You will see:

DRIVER    VOLUME NAME
local     appdata

What it means: Always pass a named volume.

Step 5: VOLUME freezes the folder

Why: Changes after VOLUME in the Dockerfile are lost.

What: Write files into the folder before the VOLUME line, not after.

Example 13: Lost change

Type this:

printf 'FROM alpine:3.20\nVOLUME /data\nRUN echo seed > /data/seed\n' > Dockerfile
docker build -t v3 . && docker run --rm v3 ls /data

You will see:

(no output)

What it means: RUN after VOLUME was thrown away.

Example 14: Right order

Type this:

printf 'FROM alpine:3.20\nRUN mkdir /data && echo seed > /data/seed\nVOLUME /data\n' > Dockerfile
docker build -t v4 . && docker run --rm v4 ls /data

You will see:

seed

What it means: Seed copied into the new volume.

Example 15: Official example

Type this:

docker image inspect mysql:8.4 -f '{{json .Config.Volumes}}'

You will see:

{"/var/lib/mysql":{}}

What it means: Databases declare their data folder.

Step 6: LABEL adds metadata

Why: Who owns this image? Which commit?

What: LABEL key=value pairs. Use the org.opencontainers.image keys.

Example 16: Add

Type this:

printf 'FROM alpine:3.20\nLABEL org.opencontainers.image.title="booking-api" org.opencontainers.image.version="1.4.0" team="payments"\n' > Dockerfile
docker build -t l1 . && docker image inspect l1 -f '{{json .Config.Labels}}'

You will see:

{"org.opencontainers.image.title":"booking-api","org.opencontainers.image.version":"1.4.0","team":"payments"}

What it means: Stored in config.

Example 17: From the command line

Type this:

docker build --label org.opencontainers.image.revision=$(git rev-parse --short HEAD) -t l2 . && docker image inspect l2 -f '{{index .Config.Labels "org.opencontainers.image.revision"}}'

You will see:

9f3c2e1

What it means: CI adds the commit.

Example 18: Filter

Type this:

docker images --filter label=team=payments --format '{{.Repository}}'

You will see:

l1
l2

What it means: Find all images of a team.

Step 7: STOPSIGNAL

Why: Some apps shut down cleanly only on a special signal.

What: STOPSIGNAL sets what docker stop sends first. Default is SIGTERM.

Example 19: nginx graceful

Type this:

docker image inspect nginx:1.27 -f '{{.Config.StopSignal}}'

You will see:

SIGQUIT

What it means: nginx finishes requests on SIGQUIT.

Example 20: Set your own

Type this:

printf 'FROM alpine:3.20\nSTOPSIGNAL SIGINT\nCMD ["sleep","300"]\n' > Dockerfile
docker build -t s1 . && docker image inspect s1 -f '{{.Config.StopSignal}}'

You will see:

SIGINT

What it means: Stored.

Example 21: Grace time

Type this:

docker run -d --name s1 --stop-timeout 30 s1 && time docker stop s1

You will see:

s1
real 0m0.3s

What it means: sleep exits on SIGINT at once; timeout is the maximum wait.

HEALTHCHECKwhile runningVOLUMEdata at run timeLABELmetadata for people and toolsSTOPSIGNALat docker stopONBUILD, SHELLduring builds
Picture 2: where each instruction acts

Step 8: Health in Compose

Why: Other services should wait until a dependency is really ready, not just started.

What: Compose reads the image health check, or a healthcheck block you write. depends_on with condition service_healthy makes the API wait until the database reports healthy.

Example 22: Compose file

Type this:

printf 'services:\n  db:\n    image: postgres:16\n    environment:\n      POSTGRES_PASSWORD: example-only\n    healthcheck:\n      test: ["CMD","pg_isready","-U","postgres"]\n      interval: 5s\n      retries: 5\n  api:\n    image: h1\n    depends_on:\n      db:\n        condition: service_healthy\n' > compose.yaml && docker compose up -d

You will see:

Container demo-db-1  Healthy
Container demo-api-1  Started

What it means: The API started only after the database turned healthy.

Example 23: Check status

Type this:

docker compose ps --format '{{.Name}} {{.Status}}'

You will see:

demo-api-1 Up 20 seconds (healthy)
demo-db-1 Up 31 seconds (healthy)

What it means: Both services show their health.

Example 24: Wait in scripts

Type this:

docker compose up -d --wait && echo all healthy

You will see:

all healthy

What it means: --wait blocks until every service is healthy, which is perfect for CI test jobs.

Step 9: Watch health events

Why: On-call engineers want to know the moment health changes.

What: docker events streams health_status events. You can filter them and feed them to alerts or simple scripts.

Example 25: Stream events

Type this:

docker events --filter event=health_status --format '{{.Actor.Attributes.name}} {{.Status}}'

You will see:

h1 health_status: unhealthy

What it means: A live line every time health changes.

Example 26: Auto-heal script

Type this:

docker events --filter event=health_status:unhealthy --format '{{.Actor.ID}}' | while read id; do docker restart $id; done

You will see:

h1

What it means: Plain Docker restarts unhealthy containers only if you script it, unlike Swarm or Kubernetes.

Example 27: Last output

Type this:

docker inspect h1 -f '{{(index .State.Health.Log 0).Output}}'

You will see:

curl: (22) The requested URL returned error: 404

What it means: The check's own output tells you why it failed.

4. ONBUILD and SHELL

Step 10: ONBUILD

Why: A base image can add steps to the child build.

What: ONBUILD instructions run when another Dockerfile uses this image in FROM.

Example 28: Base

Type this:

printf 'FROM python:3.12-slim\nWORKDIR /app\nONBUILD COPY requirements.txt .\nONBUILD RUN pip install --no-cache-dir -r requirements.txt\n' > Dockerfile.base
docker build -f Dockerfile.base -t pybase .

You will see:

=> naming to docker.io/library/pybase

What it means: Triggers stored, not run.

Example 29: Child

Type this:

printf 'FROM pybase\nCOPY . .\n' > Dockerfile
docker build --progress=plain -t child . 2>&1 | grep ONBUILD

You will see:

#5 [2/3] ONBUILD COPY requirements.txt .
#6 [3/3] ONBUILD RUN pip install ...

What it means: Triggers ran in the child.

Example 30: Hidden magic

Type this:

docker image inspect pybase -f '{{json .Config.OnBuild}}'

You will see:

["COPY requirements.txt .","RUN pip install --no-cache-dir -r requirements.txt"]

What it means: Hard to see, so most teams avoid ONBUILD.

Step 11: SHELL

Why: Change the shell used by shell-form lines.

What: SHELL ["/bin/bash","-o","pipefail","-c"] makes pipes fail properly. On Windows images, PowerShell.

Example 31: Bash with pipefail

Type this:

printf 'FROM debian:12-slim\nSHELL ["/bin/bash","-o","pipefail","-c"]\nRUN false | true\n' > Dockerfile
docker build .

You will see:

ERROR: ... "/bin/bash -o pipefail -c false | true" did not complete successfully: exit code: 1

What it means: Pipe failure caught.

Example 32: Bash features

Type this:

printf 'FROM debian:12-slim\nSHELL ["/bin/bash","-c"]\nRUN echo {a,b,c}\n' > Dockerfile
docker build --progress=plain . 2>&1 | grep '0.1 a'

You will see:

#5 0.1 a b c

What it means: Brace expansion needs bash.

Example 33: Windows

Type this:

printf 'FROM mcr.microsoft.com/windows/servercore:ltsc2022\nSHELL ["powershell","-Command"]\nRUN Write-Host hello\n' > Dockerfile && tail -1 Dockerfile

You will see:

RUN Write-Host hello

What it means: PowerShell on Windows hosts.

Step 12: Read everything back

Why: Check what your instructions stored.

What: docker image inspect shows Healthcheck, Volumes, Labels and StopSignal together.

Example 34: Health config

Type this:

docker image inspect h1 -f '{{json .Config.Healthcheck.Test}}'

You will see:

["CMD-SHELL","curl -fsS http://localhost/ || exit 1"]

What it means: Stored test.

Example 35: All at once

Type this:

docker image inspect mysql:8.4 -f '{{json .Config.Volumes}} {{.Config.StopSignal}}'

You will see:

{"/var/lib/mysql":{}} 

What it means: Volume set, default stop signal.

Example 36: Lint

Type this:

docker run --rm -i hadolint/hadolint:v2.12.0 < Dockerfile.base

You will see:

(no output)

What it means: Lint passes.

5. Common mistakes and how Neha avoids them

Mistake one: no health check. Stuck apps look Up. Neha adds HEALTHCHECK or a Kubernetes probe to every service.

Mistake two: health checks that call heavy pages. They slow the app. Neha uses a light /health endpoint.

Mistake three: RUN after VOLUME. The change disappears. Neha writes files first and declares VOLUME last.

Mistake four: images with no owner. Neha adds team and revision labels in CI.

Mistake five: hidden ONBUILD triggers. Neha prefers explicit steps in each Dockerfile.

Check yourself

Q1. What does exit 1 in a health check mean?

Unhealthy.

Q2. What does --start-period do?

Ignores failures while the app starts.

Q3. Why avoid RUN after VOLUME?

Changes there are discarded.

Q4. Which signal does nginx use to stop gracefully?

SIGQUIT.

Friends, Up is not the same as healthy. Add a health check, label your images, and choose the right stop signal. Did you understand? Let us try it once more. Add a HEALTHCHECK to one of your images and break it on purpose to watch it turn unhealthy.

Ravindra Bagale's Tip

Kubernetes ignores the Dockerfile HEALTHCHECK. Still add it for Docker and Compose, and copy the same check into liveness and readiness probes.

Lab: try at home

  1. Add a health check to nginx and break it.
  2. Use --start-period for a slow app.
  3. Prove RUN after VOLUME is lost.
  4. Add OCI labels and filter by them.
  5. Check nginx StopSignal.
  6. Use SHELL with pipefail.

Recap

  • HEALTHCHECK shows real health.
  • Tune interval, timeout, retries, start-period.
  • VOLUME marks data; name your volumes.
  • LABEL adds owner and revision.
  • STOPSIGNAL controls graceful stop.
  • ONBUILD is hidden; SHELL changes the build shell.

Ravi Sir's Cards for this lesson

मराठी cards