Ravindra BagaleCourses & study guides मराठी

Chapter 56: Docker CI/CD with Jenkins and ECR, Deploy to Amazon ECS, Troubleshooting, Best Practices and Interview Questions

56.5 Docker best practices: a checklist for every service

Friends, we have learned many separate good habits in this course. In a real team, people forget them unless they are written as a checklist that every service must pass before it goes live. Today we build that checklist, twelve items, and for every item we run a small command that proves the service passes or fails. By the end you can review any Dockerfile or container in ten minutes.

1. Start from a story

At Razorpay, every new service goes through a production readiness review before it handles real payments. A platform engineer named Kavya runs the Docker part of that review.

She does not read the Dockerfile and give opinions. She runs twelve small checks. Each check prints pass or fail. A service with any fail goes back to the team with the exact command that failed.

Teams liked it because it was fair and fast. After a few months most teams ran the same checks in their own CI, and reviews took minutes instead of days.

Today we write Kavya's twelve checks.

Imagepinned base, small, scannedBuilddockerignore, cache order, no secretsRunnon-root, read-only, limitsOperatehealth, logs, restart, labelsReview resultall pass, then go live
Picture 1: the checklist in four groups

2. Image checks

Step 1: 1. Pin the base image

Why: Builds must be repeatable.

What: FROM node:latest changes without warning. Pin a version tag, and for the strictest teams also the digest, so the same Dockerfile gives the same image next month.

Example 1: Find latest

Type this:

grep -n '^FROM' Dockerfile

You will see:

1:FROM node:latest

What it means: Fail: not pinned.

Example 2: Get the digest

Type this:

docker pull -q node:20.17-slim && docker inspect --format '{{index .RepoDigests 0}}' node:20.17-slim

You will see:

node@sha256:4a1c...e9

What it means: The exact image fingerprint.

Example 3: Pinned line

Type this:

sed -i 's|^FROM node:latest|FROM node:20.17-slim@sha256:4a1c...e9|' Dockerfile && head -1 Dockerfile

You will see:

FROM node:20.17-slim@sha256:4a1c...e9

What it means: Pass. Renovate or Dependabot can bump it.

Step 2: 2. Keep the image small

Why: Fast and fewer holes.

What: Set a size budget per service, for example 300 MB, and fail the review above it. The previous lesson showed how to shrink.

Example 4: Measure

Type this:

docker image ls shop-api:1.4 --format '{{.Size}}'

You will see:

248MB

What it means: Under budget.

Example 5: Scripted check

Type this:

S=$(docker image inspect --format '{{.Size}}' shop-api:1.4); [ $S -lt 300000000 ] && echo PASS || echo FAIL

You will see:

PASS

What it means: Size in bytes against the budget.

Example 6: Layer count

Type this:

docker image inspect --format '{{len .RootFS.Layers}}' shop-api:1.4

You will see:

9

What it means: Few layers is a sign of a tidy Dockerfile.

Step 3: 3. Scan with zero CRITICAL

Why: Known holes block release.

What: Run Trivy or Docker Scout and fail on CRITICAL. Rescan weekly, because new holes are found in old images.

Example 7: Scan

Type this:

trivy image -q --exit-code 1 --severity CRITICAL shop-api:1.4 ; echo exit=$?

You will see:

Total: 0 (CRITICAL: 0)
exit=0

What it means: Pass.

Example 8: Docker Scout

Type this:

docker scout quickview shop-api:1.4

You will see:

Target  shop-api:1.4  0C 2H 5M 11L

What it means: Zero critical, two high to plan.

Example 9: Ignore file for accepted risks

Type this:

printf '%s\n' 'CVE-2024-0001  # no fix yet, not reachable, review 2026-12-01' > .trivyignore && cat .trivyignore

You will see:

CVE-2024-0001  # no fix yet, not reachable, review 2026-12-01

What it means: Every exception has a reason and a date.

3. Build checks

Step 4: 4. A .dockerignore exists

Why: Small context, no secrets.

What: No .dockerignore means .git, .env and node_modules may enter the image.

Example 10: Exists

Type this:

test -f .dockerignore && echo PASS || echo FAIL

You will see:

PASS

What it means: Good.

Example 11: Covers the risky files

Type this:

for f in .git .env node_modules; do grep -qx "$f" .dockerignore && echo "$f ok" || echo "$f MISSING"; done

You will see:

.git ok
.env ok
node_modules ok

What it means: All three listed.

Example 12: No .env in the image

Type this:

docker run --rm --entrypoint sh shop-api:1.4 -c 'ls -a /app | grep -c env || true'

You will see:

0

What it means: Nothing leaked.

Step 5: 5. No secrets in layers

Why: History is public to anyone with the image.

What: ARG and ENV values and copied files stay in the image history forever. Use BuildKit secrets for build-time tokens and runtime secrets for passwords.

Example 13: Search history

Type this:

docker history --no-trunc --format '{{.CreatedBy}}' shop-api:1.4 | grep -iE 'password|token|secret' || echo PASS

You will see:

PASS

What it means: No secret words in any layer command.

Example 14: Search env

Type this:

docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' shop-api:1.4 | grep -iE 'pass|key' || echo PASS

You will see:

PASS

What it means: No password in ENV.

Example 15: BuildKit secret

Type this:

docker build --secret id=npmrc,src=$HOME/.npmrc -t shop-api:1.5 .

You will see:

=> [build 3/6] RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

What it means: The token is used during the build and never saved.

Step 6: 6. Layer order for cache

Why: Fast CI.

What: Dependency files are copied and installed before the source code, so most builds reuse the dependency layer.

Example 16: Check order

Type this:

grep -nE '^(COPY|RUN)' Dockerfile

You will see:

3:COPY package*.json ./
4:RUN npm ci --omit=dev
5:COPY . .

What it means: Pass: install before source.

Example 17: Second build

Type this:

docker build -t shop-api:1.5 . 2>&1 | grep -c CACHED

You will see:

5

What it means: Most layers cached.

Example 18: Build time

Type this:

time docker build -q -t shop-api:1.5 .

You will see:

real 0m4.3s

What it means: Seconds, not minutes.

4. Run checks

Step 7: 7. Non-root user

Why: A break-in should not be root.

What: The image must set USER to a normal user. Check both the image config and the running process.

Example 19: Image config

Type this:

docker image inspect --format '{{.Config.User}}' shop-api:1.4

You will see:

node

What it means: Pass. Empty means root, which fails.

Example 20: Running process

Type this:

docker run --rm shop-api:1.4 id -u

You will see:

1000

What it means: Not zero.

Example 21: Fix when it fails

Type this:

printf '%s\n' 'RUN useradd -r -u 10001 app' 'USER 10001' >> Dockerfile && tail -2 Dockerfile

You will see:

RUN useradd -r -u 10001 app
USER 10001

What it means: Two lines.

Step 8: 8. Read-only root and dropped capabilities

Why: Attackers cannot change files or gain powers.

What: Run with --read-only, a tmpfs for /tmp, --cap-drop ALL and no-new-privileges. If the app still works, the service passes.

Example 22: Hardened run

Type this:

docker run -d --name h --read-only --tmpfs /tmp --cap-drop ALL --security-opt no-new-privileges -p 3000:3000 shop-api:1.4 && sleep 2 && curl -s localhost:3000/health

You will see:

{"status":"ok"}

What it means: Pass: works hardened.

Example 23: Write test

Type this:

docker exec h sh -c 'touch /app/x' ; echo exit=$?

You will see:

touch: /app/x: Read-only file system
exit=1

What it means: Files cannot be changed.

Example 24: Capabilities

Type this:

docker exec h grep CapEff /proc/1/status && docker rm -f h

You will see:

CapEff: 0000000000000000
h

What it means: No special powers.

Step 9: 9. Memory and CPU limits

Why: One service must not starve the host.

What: Every container in production has a memory limit and a CPU limit. In Compose put them under deploy resources limits.

Example 25: Check a running container

Type this:

docker inspect --format 'mem={{.HostConfig.Memory}} cpu={{.HostConfig.NanoCpus}}' api

You will see:

mem=536870912 cpu=1000000000

What it means: 512 MB and 1 CPU. Zero would fail.

Example 26: Compose

Type this:

services:
  api:
    image: shop-api:1.4
    deploy:
      resources:
        limits: { cpus: '1.0', memory: 512M }

You will see:

(compose.yaml)

What it means: Limits written in the file.

Example 27: Find unlimited ones

Type this:

docker ps -q | xargs docker inspect --format '{{.Name}} {{.HostConfig.Memory}}' | awk '$2==0{print $1" FAIL"}'

You will see:

/legacy-cron FAIL

What it means: One container to fix.

5. Operate checks

Step 10: 10. A health check

Why: Docker and orchestrators must know if it works.

What: HEALTHCHECK in the image or healthcheck in Compose. The command must test the app, not just that the process exists.

Example 28: Image has one

Type this:

docker image inspect --format '{{json .Config.Healthcheck.Test}}' shop-api:1.4

You will see:

["CMD-SHELL","curl -fs localhost:3000/health || exit 1"]

What it means: Pass.

Example 29: Status

Type this:

docker inspect --format '{{.State.Health.Status}}' api

You will see:

healthy

What it means: Working.

Example 30: Missing one

Type this:

docker image inspect --format '{{.Config.Healthcheck}}' legacy:2.0

You will see:

<nil>

What it means: Fail: add HEALTHCHECK.

Step 11: 11. Logs to stdout with limits

Why: Logs must be readable and not fill the disk.

What: The app writes to stdout and stderr, not files inside the container. The log driver has max-size and max-file, or ships logs centrally.

Example 31: Log config

Type this:

docker inspect --format '{{.HostConfig.LogConfig.Type}} {{.HostConfig.LogConfig.Config}}' api

You will see:

json-file map[max-file:3 max-size:10m]

What it means: Pass: rotated.

Example 32: Logs appear

Type this:

docker logs --tail 1 api

You will see:

{"level":"info","msg":"listening","port":3000}

What it means: Structured JSON to stdout.

Example 33: No log files inside

Type this:

docker exec api sh -c 'find / -name "*.log" -size +1M 2>/dev/null | wc -l'

You will see:

0

What it means: Nothing hidden inside.

Step 12: 12. Restart policy, labels and graceful stop

Why: Operations details.

What: Use restart unless-stopped or an orchestrator, add OCI labels for owner and source, and make sure the app stops cleanly on SIGTERM within the stop timeout.

Example 34: Restart policy

Type this:

docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' api

You will see:

unless-stopped

What it means: Pass.

Example 35: Labels

Type this:

docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}} {{index .Config.Labels "team"}}' shop-api:1.4

You will see:

a1b2c3d payments

What it means: Who owns it and which commit.

Example 36: Graceful stop

Type this:

time docker stop api

You will see:

api
real 0m0.6s

What it means: Under a second, so it handles SIGTERM. Ten seconds would mean it was killed.

Team submits the serviceimage tag and compose fileRun the 12 checksscript prints PASS or FAILAny FAIL?send the failing command backAll PASSapprove for productionSame checks in CIevery build, forever
Picture 2: the review flow

6. Common mistakes and how Kavya avoids them

Mistake one: reviews based on opinions. Every item in Kavya's list is a command with a clear result.

Mistake two: a checklist that nobody runs again. The checks move into CI so they run on every build.

Mistake three: too many items. Twelve items cover the real risks; more would be ignored.

Mistake four: exceptions without dates. Every accepted risk has a reason and a review date.

Mistake five: checking only the image and never the running container. Limits, restart policy and log settings live on the container, so Kavya always inspects the real running service on the real server, not only the Dockerfile in the repository.

Check yourself

Q1. Why pin the base image?

So the same Dockerfile builds the same image later.

Q2. How do you prove a container is not root?

docker image inspect User and docker run id -u.

Q3. Name two operate checks.

Health check and logs to stdout with rotation.

Q4. How do you check for secrets in an image?

Search docker history and the image ENV for secret words.

Friends, image, build, run and operate: twelve checks, each a single command with a pass or fail. Did you understand? Let us try it once more. Put all twelve commands in one script, run it against one of your images, fix every fail, and add the script to your Jenkins pipeline.

Ravindra Bagale's Tip

A checklist only works if a machine runs it. Put it in CI on day one, not after the first incident.

Lab: try at home

  1. Pin the base image of one service with tag and digest.
  2. Write a size budget check.
  3. Scan with Trivy and record exceptions with dates.
  4. Search image history and ENV for secrets.
  5. Run the container hardened and fix what breaks.
  6. Add a health check and log rotation.
  7. Put all twelve checks in one script and run it in CI.

Recap

  • Image: pinned, small, scanned.
  • Build: dockerignore, no secrets, cache order.
  • Run: non-root, read-only, limits.
  • Operate: health check, logs, restart, labels, graceful stop.
  • Every check is a command; run them in CI.