ToolSura
    ToolSura
    HomeTools
    Blog
    ToolSura
    ToolSuraPrivacy-First Tools

    Free utilities that run in your browser. No trackers, no accounts, no uploads.

    All Systems Operational

    Product

    • Free Online Tools
    • Contact
    • FAQs
    • Feedback
    • About

    Legal

    • Privacy Policy
    • Cookie Policy
    • Terms & Conditions
    • Disclaimer

    Resources

    • Blog
    • Brand
    • Help

    Social Links

    • Bluesky
    • Mastodon
    • X
    • Product Hunt
    • GitHub
    • LinkedIn
    • DEV.to
    • YouTube

    © 2026 ToolSura. Free tools that run in your browser.

    Remote-First / Based in India

    Technical Manifesto

    Private • Client-Side • No Uploads

    ToolSura on Nick Launches
    Browser-Native
    Privacy-First
    Skip to main content
    Toolsura
    developer-tools
    A
    Abhay Khant

    Dockerfile best practices: a step-by-step guide

    November 1, 2026 · 6 min read

    A practical guide to writing safe Dockerfiles: multi-stage builds, non-root users, COPY vs ADD, healthchecks, and image scanning.

    A terminal window showing a Dockerfile with multi-stage build instructions, a HEALTHCHECK line, and a non-root USER directive beside a small container icon.
    A terminal window showing a Dockerfile with multi-stage build instructions, a HEALTHCHECK line, and a non-root USER directive beside a small container icon.

    A Dockerfile is a build recipe, not a script. Every instruction adds a layer to an image, and the order you write those instructions changes how fast CI rebuilds and how large the shipped image becomes. Following dockerfile best practices means smaller images, faster rebuilds, and fewer surprises in production: no secrets baked into layers, no root processes, and base images you can reproduce exactly.

    This guide covers the instructions that matter — pinning base images, multi-stage builds, COPY versus ADD, non-root users, layer caching, .dockerignore, healthchecks, and image scanning. Each section shows what to do and why, backed by Docker's official building best practices, the Dockerfile reference, and the OWASP Docker Security Cheat Sheet. No statistics are invented here; every number below is a product-specified default documented by Docker or OWASP.

    How it works: five steps from Dockerfile to image

    1. Choose a base image. Start from a Docker Official Image or a Verified Publisher image on Docker Hub. A minimal base such as Alpine (under 6 MB) cuts download size, attack surface, and vulnerability count before you write a single instruction.
    2. Write instructions in order. Docker executes FROM, RUN, COPY, and CMD in the order they appear, and each one creates a layer. The sequence determines what the build cache can reuse and when it must start over.
    3. Send a trimmed build context. The builder receives your context directory, so keep it small. .dockerignore is read before the context is sent and excludes node_modules, .git, and credentials with .gitignore-like globs, as described in Docker's build context docs.
    4. Build in stages. Multiple FROM instructions name build stages, and each stage starts from a clean state. Later stages copy only the artifacts they need, so the final image holds what runs the app, not what built it.
    5. Run and verify it. Configure a non-root user, add a HEALTHCHECK, and scan the image before it reaches production.

    Dockerfile best practices: choose and pin a base image

    Tags are mutable: FROM alpine:3.21 can resolve to a different patch version on a later rebuild, leaving no audit trail and risking breaking changes. Pin to a digest — FROM alpine:3.21@sha256:... — for reproducible builds, and let Dependabot (package-ecosystem: "docker") open pull requests to keep tags and digests current. Docker Scout's "No outdated base images" policy and docker scout recommendations detect outdated pins; see the Docker Scout docs. Rebuild regularly with --pull so fresh base images flow through, and consider one base image for build and test and a slimmer one for production.

    Multi-stage builds ship less

    Multi-stage builds separate the build environment from the runtime output, so the final image contains only the files needed to run the app. Name a stage with FROM golang:1.24 AS builder, then copy artifacts into the final stage with COPY --from=builder. Each FROM clears prior state, so stages are isolated, and multiple stages can run build steps in parallel, cutting host memory use and load time. The classic pattern is a heavier image for build and test, and a slim image for production.

    Dockerfile best practices: layer caching and .dockerignore

    Docker reuses the build cache for each RUN instruction in order, and cache invalidation happens on ADD or COPY changes. That makes layer order your biggest build-speed lever: copy dependency files first and install dependencies before copying application code, so a code change does not bust the dependency cache. Chain apt-get update with apt-get install in the same RUN; otherwise a later package install reuses a stale apt-get update cache and installs outdated versions. Add --no-install-recommends and rm -rf /var/lib/apt/lists/* in that same layer to cut image size. Pin OS packages as package=1.3.* or exact versions so version changes force a deliberate cache bust instead of silent upgrades.

    COPY vs ADD: use COPY by default

    COPY does basic copying from the build context or a stage. ADD does that too, but it additionally fetches remote HTTPS or Git URLs and auto-extracts tar archives — behavior that adds unpredictability to a build. OWASP lists "avoid the use of ADD in favor of COPY" as a commonly checked Dockerfile issue (Rule #9), and Docker's guidance prefers COPY. Use ADD only when downloading a remote artifact is part of the build, combined with --checksum. The safer replacement otherwise is wget plus tar in a RUN step, or a bind mount, so every byte in the image is intentional and scannable.

    Run as a non-root user

    Create a user and group in the Dockerfile with RUN groupadd ... && useradd ..., then switch with USER <name>. Assign an explicit UID/GID, because Docker-assigned UIDs are non-deterministic across rebuilds. Avoid sudo — its TTY and signal behavior is unpredictable inside a container — and use gosu instead. Do not flip USER back and forth across the file. OWASP calls configuring an unprivileged user "the best way to prevent privilege escalation attacks." Note that USER bakes a user into the image while docker run -u overrides it at runtime; on OpenShift, random UIDs override both, so design the application to run as any UID and write only to world-writable directories.

    HEALTHCHECK example and image scanning

    HEALTHCHECK [OPTIONS] CMD command tells Docker how to test that a container is still working, for example a stuck web server process. Documented defaults are --interval=30s, --timeout=30s, --retries=3, --start-period=0s, and --start-interval=5s (start interval requires Engine 25.0+), per the HEALTHCHECK reference. Exit codes are 0 for healthy, 1 for unhealthy, and 2 reserved; only one HEALTHCHECK belongs in a Dockerfile, and its output is stored in the health status, queryable via docker inspect (first 4096 bytes).

    HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
      CMD curl -f http://localhost:8080/healthz || exit 1
    

    Orchestrators can replace unhealthy containers instead of restarting blindly. Kubernetes liveness and readiness probes cover similar ground but are evaluated by the kubelet, so you can run both: Docker reports health to the daemon, and Kubernetes decides when to send traffic or restart a pod.

    For scanning, free options include Clair, Grype, and Trivy; commercial or free-tier options include Snyk, Anchore, Docker Scout, JFrog XRay, and Qualys. Docker Scout builds an SBOM per image and matches it against a continuously updated vulnerability database. OWASP's scan rules check for the USER directive, a pinned base image, pinned OS packages, no ADD, and no curl-bashing in RUN.

    A production Dockerfile, put together

    Here is a single, copy-pasteable example that combines multi-stage builds, digest-pinned base images, .dockerignore, a non-root USER, and a HEALTHCHECK.

    FROM golang:1.24@sha256:__BUILDER_DIGEST__ AS builder
    WORKDIR /src
    COPY go.mod go.sum ./
    RUN go mod download
    COPY . .
    RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server
    
    FROM alpine:3.21@sha256:__RUNTIME_DIGEST__
    RUN addgroup -S app && adduser -S app -G app
    WORKDIR /app
    COPY --from=builder /app/server /app/server
    USER app
    HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
      CMD curl -f http://localhost:8080/healthz || exit 1
    ENTRYPOINT ["/app/server"]
    

    Replace __BUILDER_DIGEST__ and __RUNTIME_DIGEST__ with the digests returned by docker scout recommendations for your chosen images. Add a .dockerignore at the context root listing node_modules, .git, *.md, and anything you do not want inside the image.

    Related reading

    • Automating CI — CI image builds and pipeline steps that consume Dockerfiles.
    • Install, build, first run — build ordering and first-run checks that pair well with image builds.
    • Block silent Windows app installs — deployment control on the client side.
    • Big runs, no throttling — running long jobs without throttling, relevant to CI image builds.
    Abhay Khant

    Written by

    Abhay Khant

    Hi, I'm Abhay Khant

    I'm a developer, builder, and technology enthusiast interested in turning ideas into useful products.

    I enjoy working across web development, cybersecurity, automation, SEO, and content creation. Rather than only learning technologies individually, I like putting them together to build things that people can use.

    One of my main projects is Toolsura, a free online utility platform that brings practical tools into one place. I'm building Toolsura as a product and a learning project where I can explore development, infrastructure, SEO, content distribution, automation, and product growth.

    What I Build

    My work currently revolves around several areas:

    • Web development with Next.js, React, Tailwind CSS, and modern web technologies

    • Backend development with Go, APIs, databases, and server-side systems

    • Automation using n8n, APIs, Docker, and connected services

    • SEO and content systems for building organic search traffic

    • Cybersecurity including web security, bug bounty, SOC concepts, and malware analysis

    • Content creation including video editing, YouTube content, graphics, and promotional videos

    I enjoy going beyond tutorials and experimenting with complete systems, from the frontend interface all the way to the backend, deployment, automation, and distribution.

    Toolsura

    Toolsura is one of the projects I'm most actively building.

    The idea is simple: create useful, accessible online tools that solve small problems quickly.

    Toolsura includes utilities for things such as text, JSON, URLs, PDFs, images, security checks, and other everyday technical tasks.

    Building it has also become a practical way for me to learn how a modern web product works.

    That means working on:

    Product → Development → Infrastructure → SEO → Content → Distribution → Automation → Growth

    Technology & Learning

    I'm particularly interested in understanding how systems work rather than simply learning how to use them.

    My development work has included technologies such as:

    Next.js · React · TypeScript · Tailwind CSS · Go · Gin · GORM · PostgreSQL · MongoDB · Docker · RabbitMQ · MinIO · Strapi

    I've also worked with Linux environments and tools such as Kali Linux, Termux, and related security tooling.

    My cybersecurity interests include web application security, bug bounty research, SOC analysis, and malware analysis.

    Building in Public

    I also create content around the things I build and learn.

    This includes:

    • Tutorials and technical articles

    • YouTube videos

    • Product demonstrations

    • Visual explainers

    • Promotional videos

    • SEO-focused educational content

    My goal is to turn projects and experiments into useful resources that other people can learn from.

    Why I Build

    For me, building is one of the best ways to learn.

    Instead of learning a technology in isolation, I prefer starting with a real problem and figuring out what is required to solve it.

    Sometimes that means writing code. Other times it's debugging a deployment at midnight, or working out why a page isn't being indexed, why an API isn't behaving correctly, or how to automate something that would otherwise take hours.

    Those problems are part of the process.

    Every project gives me another opportunity to understand how technology works in the real world.

    What I'm Working Toward

    I'm continuing to develop my skills across software engineering, cybersecurity, automation, and digital products.

    At the same time, I'm building Toolsura into a useful platform and experimenting with different ways to create, distribute, and grow technical products.

    There is still a lot to learn, but that's exactly what makes building interesting.

    Let's Connect

    If you're interested in web development, cybersecurity, automation, SEO, online tools, or building products, feel free to connect.

    I'm always interested in learning, experimenting, and building something useful.

    Build. Learn. Improve. Repeat.

    Share

    Frequently Asked Questions

    PreviousWhat Is a .gitignore File? Gitignore ExplainedNextHTTP Security Headers Explained: CSP, HSTS, X-Frame-Options

    Comments

    Leave a Review

    Rate this tool
    Overall Rating
    Spam Protection Active