What Is a RUN Instruction in Docker?
A friendly, step-by-step look at the Dockerfile instruction that actually builds your image: what it does behind the scenes, why every RUN line becomes a permanent layer, and how to write it well.
A Dockerfile is really just a numbered list of instructions telling Docker how to assemble an image, one step at a time. Among those instructions, one does more of the actual heavy lifting than any other: RUN. It is the instruction that installs software, compiles code, creates folders, and generally shapes what ends up inside the final image. Every other instruction mostly copies things in or sets configuration; RUN is where things actually happen.
This guide is dedicated entirely to that one instruction. You will learn exactly what happens inside the engine each time a RUN line executes, why it permanently changes the image being built, the two different ways to write it, how it interacts with Docker’s build cache, and the mistakes that quietly bloat images or break builds for people just getting started. Every new term is explained plainly and in order, so no earlier Docker knowledge is assumed.
1What Does RUN Actually Do?
Before looking at syntax, it helps to be precise about what this one instruction is responsible for.
A Dockerfile is a plain text file containing an ordered list of build instructions. When you ask Docker to build an image from it, each instruction is processed one after another, from top to bottom. The RUN instruction tells Docker to execute a specific command inside a temporary environment that looks exactly like the image built so far, and then to permanently save whatever changed on the file system as a result of that command into a brand-new, read-only layer.
In other words, RUN is how you make the image itself different from its starting point. Installing a package, creating a folder, downloading a file, compiling a program, all of these are typically done with a RUN instruction, because each one leaves behind file system changes that need to become a permanent part of the finished image, available every time a container is later started from it.
Think of building an image like assembling a piece of flat-pack furniture using an instruction leaflet. Most steps on the leaflet just say “take part A and place it next to part B,” which is closer to what the COPY instruction does. A RUN instruction is more like a step that says “now drill four holes and attach the legs permanently.” Once that step is done, the result is baked into the furniture; you cannot get back to the version without legs without starting over from an earlier stage.
What RUN is not
RUN is easy to confuse with two other instructions that also look like they “do something”: CMD and ENTRYPOINT. Those two instructions do not run anything while the image is being built. Instead, they simply record which command should be executed later, the moment a container is started from the finished image. RUN executes immediately, during the build, and its effects are frozen into the image forever. CMD and ENTRYPOINT execute only later, every time a new container starts, and never change the image itself. This distinction is important enough that it gets its own dedicated chapter later in this guide.
It is worth being clear about why so much attention gets paid to a single instruction. A Dockerfile is typically written once and then rebuilt many times over the life of a project, sometimes many times in a single day as part of an automated pipeline. Every one of those rebuilds re-encounters the exact same RUN instructions, so any inefficiency written into one of them, an unnecessary download, a missed cleanup step, a poor ordering choice relative to the build cache, is not paid once but repeated on every single rebuild from that point forward. A few extra seconds or a few extra megabytes might feel trivial the first time a Dockerfile is written, but multiplied across hundreds or thousands of future builds, across an entire team or an entire company, those small inefficiencies become a meaningful, ongoing cost in both time and money.
If a Dockerfile instruction changes what files exist inside the image, it is probably RUN, or one of its close cousins COPY and ADD, which copy files in rather than executing a command. If an instruction only changes what happens later, when a container is started, it is probably CMD, ENTRYPOINT, or an environment variable set with ENV.
RUN, COPY and ADD: the three instructions that actually change the image
RUN is not the only instruction capable of leaving a permanent mark on the image being built. COPY brings specific files or folders from the build context directly into the image, without executing anything at all, which is how application source code usually gets in. ADD does something similar to COPY, but additionally understands how to fetch a file from a remote address and automatically unpack certain compressed archive formats as it copies them in. RUN is different from both because it does not simply place existing files into the image; it executes an arbitrary command and captures whatever that command produces, which might be new files created from nothing, existing files modified in place, or files removed entirely. This is exactly why RUN is considered the most powerful, and therefore the most consequential, of the three.
2Core Concepts You Need First
A handful of words appear constantly around RUN. Getting comfortable with them now makes every later chapter click into place.
Dockerfile
A plain text file listing, in order, every instruction needed to build an image, including one or more RUN instructions.
Image
The finished, read-only package produced once every instruction in the Dockerfile, including every RUN, has been carried out and stacked into layers.
Layer
One frozen slice of file system changes. Each RUN instruction normally produces exactly one new layer, stacked on top of the ones before it.
Build container
A short-lived, temporary container the builder creates behind the scenes just to execute a RUN instruction’s command before capturing the result.
BuildKit
The modern engine component that actually processes a Dockerfile’s instructions, including RUN, and is the default builder in current versions of Docker.
Build cache
A record of previously executed instructions that lets the builder skip re-running a RUN instruction whose command and inputs have not changed.
Build context
The set of local files and folders sent to the builder, which COPY and ADD instructions can pull from, though RUN itself does not directly touch them.
Shell form and exec form
The two different ways a RUN instruction’s command can be written, covered in full in a later chapter, each with slightly different behaviour.
RUN in one sentence
If you remember only one sentence from this chapter, make it this: a RUN instruction executes a command inside a temporary build container based on the image so far, and then freezes whatever that command changed on disk into a new, permanent layer of the image being built.
Because every RUN instruction builds directly on top of the layer produced by the one before it, the order of RUN instructions in a Dockerfile is not just cosmetic. Moving a RUN instruction earlier or later can change what files exist when it executes, and, as later chapters explain, can also change how effectively the build cache is able to help on future builds.
3Architecture: What Executes a RUN Instruction
Just like running a finished container, building an image with RUN instructions passes through a short chain of specialised programs.
flowchart LR
CLI["Docker CLI: docker build"] --> API["Engine API"]
API --> D["dockerd: the Docker daemon"]
D --> BK["BuildKit: reads the Dockerfile"]
BK -->|"for each RUN instruction"| TMP["Temporary build container"]
TMP --> R["runc: executes the command"]
R --> DIFF["Capture file system changes"]
DIFF --> LAYER["Commit changes as a new image layer"]
LAYER -->|"next instruction"| BK
When you run a build command, the Docker CLI sends your Dockerfile and build context to the daemon over the Engine API, exactly as described for other Docker operations. The daemon hands the actual work of interpreting the Dockerfile to BuildKit, the modern build engine that has been the default since 2023. BuildKit reads the Dockerfile instruction by instruction, and whenever it reaches a RUN instruction, it asks the lower-level runtime, runc, to start a short-lived container based on the image as it exists at that exact point in the build.
That command executes inside this temporary container, with full access to whatever the image contains so far, but with no special awareness that it is part of a build rather than a normal container. Once the command finishes, BuildKit compares the container’s file system against the layer it started from, captures exactly what changed, whether files were added, modified or deleted, and commits that difference as a new, permanent, read-only layer. The temporary build container itself is then discarded; only the layer it produced survives.
Imagine a photographer documenting a room being renovated one step at a time. Before each RUN instruction, they take a snapshot of the room. A worker then makes exactly one change, say, painting a wall. Afterward, the photographer takes another snapshot, and only the difference between the two snapshots, the painted wall, is written down as “step three” in a permanent record. The worker and their tools then leave, but the painted wall stays.
Why BuildKit replaced the older builder
Before BuildKit became the default, the classic builder processed a Dockerfile’s instructions strictly one at a time, in a single, straight line, waiting for each RUN instruction to finish completely before even considering the next one. BuildKit can recognise when parts of a Dockerfile, especially across multiple build stages, do not actually depend on each other, and can run those independent instructions at the same time, which is one of the main reasons builds became noticeably faster after the switch.
The role of the build context
Before BuildKit can process a single RUN instruction, it first needs the build context, the folder and its contents that you point the build command at, which is sent to the daemon in full before the build actually starts. RUN itself does not read directly from this context; only COPY and ADD do. Even so, the build context indirectly shapes how quickly a RUN instruction gets to start, because a very large context, perhaps accidentally including a folder full of unrelated files, takes longer to transfer before the first instruction, RUN or otherwise, can even begin. This is exactly the problem a properly written ignore file, listing folders and files that should never be sent as part of the context, is meant to solve.
4Internal Working: Inside the Temporary Container
The temporary container created for a RUN instruction is not a special, stripped-down environment. It is a real container, isolated the same way any other container is.
The build container that executes a RUN instruction’s command gets its own namespaces, the Linux kernel feature that gives a process a private view of the process list, the network, and the file system, exactly as described for any other running container. It also has cgroup limits available to it, so a runaway command during a build can, in principle, be constrained the same way a misbehaving application container would be. From the point of view of the command being executed, there is nothing unusual about its environment; it genuinely believes it has its own small, disposable machine to work in.
The file system this temporary container starts with is assembled from every layer produced so far, stacked using the same copy-on-write technique used for ordinary containers: read-only layers underneath, with one thin writable layer added on top just for this one instruction. Anything the RUN command writes, downloads, installs, or deletes happens in that writable layer. Once the command finishes, BuildKit reads exactly what is different in that writable layer compared with the layers underneath it, and that difference becomes the new, permanent layer.
flowchart TB
L1["Layers built by earlier instructions"] --> W["Fresh writable layer for this RUN"]
W --> EXEC["Command executes: files are created, changed, or removed"]
EXEC --> CAP["BuildKit captures the writable layer's final contents"]
CAP --> NEW["New read-only layer added permanently to the image"]
Only changes to the file system are captured into the new layer. Anything a RUN command does that leaves no trace on disk, such as printing a message to the screen, or a background process it starts that is still running when the command exits, does not persist into the image at all. Only files that exist on disk when the command completes become part of the layer.
5Data Flow: A Build From Start to Finish
Following one full build shows exactly where RUN instructions fit among everything else that happens.
sequenceDiagram
participant U as You
participant CLI as Docker CLI
participant BK as BuildKit
participant TC as Temp container
U->>CLI: docker build .
CLI->>BK: send Dockerfile and build context
BK->>BK: FROM: pull or use base image
BK->>BK: COPY: add source files as a layer
BK->>TC: RUN instruction 1: create temp container
TC-->>BK: capture resulting layer
BK->>TC: RUN instruction 2: create new temp container
TC-->>BK: capture resulting layer
BK->>BK: CMD: record default command, no execution
BK-->>CLI: final image assembled
CLI-->>U: image ID and tag printed
A typical Dockerfile begins with a FROM instruction, which selects a starting base image rather than building anything new. Instructions like COPY then add specific files from the build context into the image as their own layers. Every RUN instruction along the way spins up its own short-lived temporary container, executes its command, and commits exactly one new layer, as covered in the previous chapter. Finally, instructions such as CMD and ENTRYPOINT do not execute or create any layer at all; they simply record metadata inside the image describing what should happen later, once a container is actually started from it.
What happens if a RUN instruction fails
If the command inside a RUN instruction exits with a non-zero status, meaning it reports that something went wrong, the entire build stops immediately at that point. No layer is committed for the failed instruction, and none of the instructions after it in the Dockerfile are ever attempted. This is a deliberate safety measure: an image built from a Dockerfile where a critical installation step silently failed partway through could look complete while actually being broken in a way that only shows up later, so Docker prefers to fail the whole build loudly and immediately instead.
When a build fails, the terminal output shows exactly which RUN instruction failed and the last portion of that command’s own output, which usually points directly at the underlying problem, such as a misspelled package name or a missing dependency that needed to be installed in an earlier RUN instruction first.
6Shell Form vs Exec Form
A RUN instruction can be written in two visibly different styles, and the difference between them is more than cosmetic.
Shell form
- Written as plain text after the word RUN, the way you would type it directly into a terminal
- Automatically executed through a shell program, commonly one called sh, which is what allows features like chaining several commands together with an ampersand symbol
- Supports shell features such as environment variable substitution, wildcards, and piping one command’s output into another
- The shell itself becomes the process that starts, with your command running as a smaller process underneath it
Exec form
- Written as a list of separate pieces in square brackets, with the command and each of its arguments spelled out individually
- Executed directly, without any shell program wrapping it, unless a shell is explicitly included as one of the listed pieces
- Does not automatically expand environment variables or support features like piping, since there is no shell present to interpret them
- Considered the more precise and predictable form, since there is no shell behaviour to account for
Why the difference actually matters
Because shell form runs your command underneath a shell process, that shell technically becomes the main process, with your actual program running as a child process it started. This distinction rarely matters for a RUN instruction specifically, since RUN’s build container is discarded after use anyway, but the same shell-versus-exec choice reappears for the CMD and ENTRYPOINT instructions, where it genuinely affects how a running container responds to a stop signal, because a stop signal sent to the shell does not always reach the child process running underneath it correctly.
Shell form is like giving someone a handwritten note in your own everyday language, trusting them to interpret abbreviations, local expressions and shorthand correctly. Exec form is like handing over a precise, itemised checklist with every single step spelled out separately, leaving nothing open to interpretation. The checklist is less flexible to write quickly, but far less likely to be misunderstood.
Because shell form relies on a shell being present inside the image to interpret it, using shell form in a very minimal base image that does not include a shell at all will cause the RUN instruction to fail outright. Exec form avoids this specific problem entirely, since it does not require a shell unless one is explicitly listed as part of the command.
Escaping and quoting inside RUN
Because shell form is interpreted by a real shell, all of the ordinary rules of that shell’s own syntax apply, including how quotation marks, spaces and special characters must be escaped to be treated literally rather than interpreted. A file path containing a space, or a value containing a character the shell treats specially, needs to be quoted correctly, exactly as it would inside an ordinary terminal session. Exec form largely sidesteps this concern, since each argument is already listed as its own separate, explicit piece rather than being reassembled and reinterpreted by a shell, which is one more reason some Dockerfile authors prefer it for commands where the exact arguments matter a great deal.
7RUN vs CMD vs ENTRYPOINT
These three instructions all involve executing a command in some sense, which is exactly why beginners mix them up so often.
| Instruction | When it executes | Effect on the image |
|---|---|---|
| RUN | Immediately, during the build | Permanently changes the image by adding a new layer |
| CMD | Never during the build; only as the default command when a container starts | No layer added; only records metadata describing a default command |
| ENTRYPOINT | Never during the build; only when a container starts, as the fixed main command | No layer added; only records metadata describing the container’s main process |
You can have any number of RUN instructions in a single Dockerfile, and each one contributes its own layer. CMD and ENTRYPOINT, by contrast, are only meaningful once, right before the final image is complete; if either is listed more than once, only the last occurrence actually takes effect, since both are simply overwriting the same piece of stored metadata rather than executing anything.
A helpful way to keep the three straight is to ask one question about each instruction you are about to write: is this something that needs to happen once, right now, to shape the image itself, or something that should happen every single time a container is later started from it? The first case is RUN. The second case is CMD or ENTRYPOINT.
A Dockerfile for a small web application might use RUN to install the programming language’s runtime and the application’s dependencies, since those steps only need to happen once, while the final line of the file uses CMD to describe how to actually start the web server, since that needs to happen fresh every single time a new container is launched from the finished image.
8RUN and the Build Cache
One of the biggest practical differences between a fast build and a slow one comes down to how well RUN instructions cooperate with Docker’s build cache.
Every time an instruction in a Dockerfile is processed, the builder checks whether it has already executed that exact instruction before, starting from an identical earlier layer, with identical inputs. If so, it skips re-running the command entirely and simply reuses the previously created layer, a shortcut known as a cache hit. If anything is different, the instruction, and every single instruction after it in the file, must be executed fresh, a cache miss that also invalidates the cache for everything downstream.
For a RUN instruction specifically, a cache hit generally requires two things to match exactly compared with a previous build: the RUN instruction’s own text must be identical, and the layer it is building on top of must also be unchanged. This is exactly why the order of instructions in a Dockerfile has real, practical consequences. Placing a RUN instruction that rarely changes, such as installing a fixed set of system packages, before a RUN instruction that changes often, such as one depending on frequently updated application code, lets the slow, rarely-changing step keep being served from cache on almost every build, while only the fast, frequently-changing step actually needs to re-run.
flowchart TB
F["FROM base image (rarely changes)"] --> R1["RUN install system packages (rarely changes)"]
R1 --> R2["RUN install application dependencies (changes occasionally)"]
R2 --> C["COPY application source code (changes every commit)"]
C --> R3["RUN build or compile step (must rerun whenever code changes)"]
style F fill:#f5f5f5
style R1 fill:#f5f5f5
Placing a RUN instruction that installs system packages after a COPY instruction that copies in frequently changing source code means that, every single time the source code changes even slightly, the cache is invalidated starting at that COPY instruction, forcing every RUN instruction after it, including the slow package installation, to execute all over again on every single build.
Cache mounts: caching without a permanent layer
Sometimes the thing worth caching between builds, such as a package manager’s own local download cache, is not something you actually want baked into a layer at all, since it would only bloat the final image without ever being needed once the software is already installed. BuildKit’s cache mount feature solves this specific tension by giving a RUN instruction access to a special cache directory that persists from one build to the next behind the scenes, without that directory’s contents ever being written into the image’s own layers. A package manager can reuse previously downloaded files across many builds, dramatically speeding up repeated installs, while the final image stays exactly as clean as if that cache had never existed.
9Combining Commands in a Single RUN
Whether to write one RUN instruction per command, or chain several commands into one RUN instruction, is a decision every Dockerfile author eventually has to make.
Because each RUN instruction produces its own permanent layer, writing five separate RUN instructions to update a package list, install several packages, and then clean up temporary download files produces five separate layers, even though the end result the author actually wanted was simply “packages installed, nothing extra left behind.” The trouble is that a layer captures the file system state at the moment its instruction finished, not the final state after every later instruction has also run. If the download cache is cleaned up in its own, later RUN instruction, the layer that originally downloaded those files still exists underneath, still taking up its full original space inside the image, even though a later layer appears to have deleted the files.
Chaining the update, install and cleanup steps together into one single RUN instruction, connected with the shell’s own command-joining syntax, means all of that activity happens inside one writable layer before it is ever captured. The temporary download files are created and deleted entirely within that one layer’s lifetime, so the final captured layer only reflects the clean end result, genuinely saving the space rather than merely hiding it under a later layer.
Imagine photographing a kitchen after cooking a meal, once after chopping vegetables, leaving peels everywhere, and again after throwing the peels away. Both photographs exist in the album forever; looking at the first one still shows a mess, even though the second one looks tidy. If you instead wait to take a single photograph until after both chopping and cleaning are finished, the album only ever shows the tidy kitchen. Combining commands in one RUN instruction is choosing to take that single, final photograph.
Combining every single instruction in an entire Dockerfile into one enormous RUN line would technically minimise layers, but it also destroys almost all of the build cache’s usefulness, since changing any tiny part of that one giant command forces the entire thing to re-run. The practical middle ground groups commands that are logically related and roughly change together, such as one combined RUN instruction for updating and installing a fixed set of system packages, while keeping steps that change independently, like installing application-specific dependencies, in their own separate RUN instructions.
10Advantages, Disadvantages and Trade-offs
RUN is indispensable, but the way it is used shapes almost everything about the resulting image’s size, speed and clarity.
Advantages
- Gives complete, ordinary command-line power during a build, so almost anything installable or scriptable on the base operating system can become part of the image
- Each instruction’s isolated, layered nature makes builds reproducible and independently cacheable
- Failures stop the build immediately and clearly, rather than silently producing a broken image
- Works identically across every machine that runs the same Dockerfile, since the temporary build container is isolated the same way any container is
Disadvantages and honest limits
- Careless use, one command per RUN instruction with no cleanup, can bloat an image with files that are logically deleted but still physically present in an earlier layer
- Poor ordering of RUN instructions relative to COPY can quietly destroy the build cache’s usefulness, making every build slow
- Shell form’s flexibility can hide subtle bugs, such as a failing command in the middle of a chain not actually stopping the whole RUN instruction, unless the Dockerfile is written carefully
- A RUN instruction that downloads something from the internet makes the build’s result depend on that resource still being available and unchanged in the future
11Security Considerations for RUN
Because RUN can execute literally any command, it is also the instruction most capable of quietly introducing a security problem into an image.
Secrets as plain arguments
Passing a password or an access token directly as part of a RUN command’s text embeds it permanently into that layer, recoverable by anyone who can inspect the image’s history, even if a later instruction appears to remove it.
Downloading and executing scripts blindly
A RUN instruction that downloads a script from the internet and immediately executes it trusts that remote source completely, with no opportunity to review what it actually does before it runs with the build’s permissions.
Pin exact package versions
Specifying an exact version for anything installed inside a RUN instruction, rather than always taking whatever the latest version happens to be, keeps a rebuilt image predictable and avoids silently inheriting an unreviewed change.
Minimise what gets installed
Every package a RUN instruction installs is also a package that could later be found to have a security vulnerability; installing only what is genuinely needed keeps that exposure as small as possible.
The situation
A RUN instruction sometimes genuinely needs a temporary credential, for example to download a private package during the build, but that credential must never end up permanently embedded in one of the image’s layers.
The response
Modern BuildKit supports a dedicated secret-mounting feature specifically for this situation, making a credential available to a single RUN instruction’s temporary container for the duration of that command only, without it ever being written into the resulting layer or visible in the image’s build history afterward.
Why it works
The credential exists only in memory for the lifetime of that one command, so even a full, careful inspection of the finished image’s layers and history reveals nothing, unlike a credential typed directly into a RUN command’s own text.
Deleting a secret file, or clearing a variable, in a later RUN instruction does not remove it from the image. The earlier layer where that secret was originally written still exists underneath, unchanged, and remains fully recoverable by anyone who examines the image’s individual layers rather than only its final, visible file system.
12Monitoring and Debugging RUN During a Build
A build that fails partway through a long chain of RUN instructions can feel opaque unless you know where to look.
By default, the builder shows a compact, live-updating summary of build progress, which is convenient for a healthy build but can hide the full output of a RUN instruction’s command. Switching the build’s progress output to a plain, non-interactive mode instead prints every line of output from every instruction in full, in the order it happened, which is often the fastest way to find the exact line inside a long installation command that actually failed.
Because each RUN instruction is captured as its own layer, another useful debugging technique is building only up to a specific instruction, deliberately stopping the Dockerfile short of a failing later step, and then starting an interactive container from that partial result to explore the file system by hand, checking exactly what state things were left in right before the failure. This turns a build failure from an abstract error message into something you can directly poke around inside.
| Situation | Useful technique |
|---|---|
| A RUN instruction fails with an unclear error | Rebuild with plain progress output to see the command’s full, uncollapsed output |
| Need to inspect the file system right before a failing step | Build only up to the instruction before the failure, then start a container from it |
| Suspect a RUN instruction is not being cached as expected | Compare the exact instruction text and check whether anything upstream changed |
| Image is larger than expected | Inspect the size contributed by each individual layer to find which RUN instruction is responsible |
Tools that inspect an image’s history can show exactly how much disk space each individual layer, and therefore each individual RUN instruction, contributed to the final image. This is frequently the quickest way to spot a RUN instruction whose downloaded files were never properly cleaned up within that same instruction.
Watching cache hits and misses as a build runs
The build output for each instruction typically indicates plainly whether it was served from cache or actually executed fresh, which is worth paying attention to on every single build, not only a failing one. A RUN instruction that is unexpectedly re-executing on nearly every build, when it was expected to be served from cache, usually points to something upstream of it changing more often than assumed, whether that is the exact instruction text itself, an argument that includes a constantly changing value, or a COPY instruction earlier in the file bringing in files that change more frequently than the author realised. Noticing this pattern early, before it becomes an accepted, permanent slowdown that everyone simply works around, is one of the more valuable habits a Dockerfile author can build over time.
13Design Patterns and Anti-patterns
Certain ways of writing RUN instructions work well again and again. Others cause the same predictable pain under a different disguise every time.
Update, install and clean in one RUN
Chaining a package list update, the actual installation, and cleanup of temporary download files into a single RUN instruction, so the wasted space never gets captured into a permanent layer in the first place.
Order by change frequency
Placing RUN instructions that rarely change near the top of the Dockerfile and ones that depend on frequently changing source code near the bottom, to get the most benefit from the build cache.
Multi-stage builds
Using RUN instructions freely, including heavy compiler tools, in an early build stage, then copying only the finished output into a clean final stage, so the heavy tools never appear in the image that actually ships.
Cache mounts for package managers
A BuildKit feature that lets a RUN instruction reuse a persistent cache directory for a package manager’s own downloads across builds, without that cache directory ever becoming part of the image’s layers.
What it looks like
Writing a separate RUN instruction for updating package lists, another for installing packages, and a third, later RUN instruction for deleting the temporary download cache those packages left behind.
Why it hurts later
The layer created by the installation step already contains the temporary download files, permanently, since they existed on disk at the moment that layer was captured. A later cleanup instruction only removes them from the visible, combined view of the final image; the earlier layer, and the space it uses, never actually shrinks.
The fix
Combine the update, install, and cleanup steps into a single RUN instruction, so the layer captured at the end already reflects the clean, final state, with nothing wasted underneath.
What it looks like
A RUN instruction that always installs whatever the newest available version of a package or tool happens to be, with no specific version mentioned at all.
Why it hurts later
Rebuilding the exact same Dockerfile weeks or months later can silently pull in a different, newer version of that package, sometimes with breaking changes, producing a subtly different image from a file that has not itself changed at all, which makes bugs extremely difficult to trace back to their real cause.
The fix
Specify an exact version wherever a package manager or download step allows it, so that rebuilding the same Dockerfile reliably reproduces the same result every time, regardless of when the build happens to run.
14Best Practices and Common Mistakes
A short, practical checklist that resolves most of the problems people run into the first time they write RUN instructions of their own.
Order for the cache
Put stable, rarely changing RUN instructions before COPY instructions that bring in frequently changing source code.
Chain related steps together
Group commands that logically belong together and should leave no leftover files, such as install-then-clean sequences, into one RUN instruction.
Pin versions explicitly
Name an exact version for anything a RUN instruction installs, so a rebuild months later still produces the same result.
Prefer exec form when precision matters
Use exec form’s explicit argument list when you want to avoid any ambiguity introduced by an underlying shell’s own interpretation.
Never bake secrets into a RUN command
Use BuildKit’s dedicated secret-mounting feature instead of typing a credential directly into a RUN instruction’s text.
Check for a non-zero exit properly
In a chain of piped commands, make sure a failure partway through the chain is not accidentally hidden by the exit status of the very last command in the pipe.
Many Linux package managers keep a local, cached list of what is available to install. Running an installation command without first refreshing that list, inside the very same RUN instruction, can silently install an outdated version, or fail outright, if the base image’s cached list is stale by the time the build actually runs.
Working with the user instruction alongside RUN
A related, often overlooked habit involves the USER instruction, which switches which user account subsequent instructions, including later RUN instructions, execute as. Many base images run as the root user by default, since installing system packages typically requires that level of access. A well-written Dockerfile often performs the RUN instructions that genuinely need elevated access first, then switches to a plain, unprivileged user with the USER instruction before any RUN instructions that do not need elevated access, such as running the application’s own test suite, and certainly before the image’s final CMD or ENTRYPOINT, so the application itself never runs with more privilege than it actually needs once a container starts from the finished image.
15Real-World and Industry Examples
The RUN instruction may look small, but how teams write it has a measurable effect on real engineering outcomes at scale.
Netflix
With thousands of internal images built continuously, disciplined RUN instruction ordering and combined install-and-cleanup steps directly translate into faster build pipelines and smaller images pulled across its infrastructure.
Amazon
Publishes official base images with carefully written RUN instructions specifically optimised for build cache efficiency and minimal final size, since these images are pulled an enormous number of times by customers building on AWS.
Popularised the multi-stage build pattern in official documentation and tooling, where heavy RUN instructions involving compilers live in an early stage that is never shipped, keeping production images lean.
GitLab
Documents and recommends combining RUN instructions and pinning exact versions as a core part of its own container image build guidance, reflecting lessons learned from operating enormous numbers of automated builds daily.
Package registries: npm and PyPI-adjacent tooling
Maintainers of official images for widely used programming language ecosystems typically publish detailed guidance specifically about writing efficient RUN instructions for that ecosystem’s own package manager, since a single small improvement, repeated across millions of downstream builds run by other developers and companies every day, adds up to a genuinely enormous collective saving in build time and bandwidth.
Across every one of these organisations, the underlying lesson is the same one covered throughout this guide: RUN instructions are cheap to write but expensive to write carelessly, and the small, disciplined habits around ordering, chaining and pinning them scale up into real savings in build time, image size and storage cost once multiplied across thousands of builds a day.
16Frequently Asked Questions
17Summary and Key Takeaways
Carry these ideas forward
- RUN executes a command inside a temporary build container and permanently freezes whatever that command changed on disk into a brand-new, read-only layer of the image.
- RUN is fundamentally different from CMD and ENTRYPOINT: RUN happens once, during the build, and changes the image itself; the other two only describe what should happen later, every time a container starts.
- Shell form runs your command through a shell and supports its conveniences; exec form runs it directly, with no shell interpretation, and is the more precise, predictable choice.
- The build cache rewards RUN instructions that are ordered from least to most frequently changing, and punishes placing a stable RUN instruction after a frequently changing COPY.
- Chaining an update, an installation, and a cleanup step into one RUN instruction keeps temporary files from ever being permanently captured into a layer in the first place.
- Secrets should never appear directly inside a RUN instruction’s own text; a dedicated secret-mounting feature keeps them out of every layer and out of the image’s build history entirely.
- Pinning exact versions inside RUN instructions is what makes rebuilding an unchanged Dockerfile produce the same reliable result today, next month, and next year.