RUN

Execute commands at build time. Learn shell vs exec form, how to keep layers lean, combine commands efficiently, and avoid bloated images.

RUN

RUN executes a command during the build phase and commits the result as a new image layer. It's how you install packages, compile code, create directories, set permissions, and do any other build-time work. Using it well is the difference between a 50MB image and a 500MB one.

Basic Syntax

# Shell form - runs in /bin/sh -c "..."
RUN apt-get update && apt-get install -y curl

# Exec form - runs directly, no shell
RUN ["apt-get", "install", "-y", "curl"]

The shell form is more common for RUN because you need shell features: &&, ||, pipes (|), redirects (>), variable expansion.

The exec form avoids the shell overhead and is useful when the base image has no shell (e.g. FROM scratch).

Installing Packages

Debian / Ubuntu (apt-get)

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        git \
        ca-certificates && \
    rm -rf /var/lib/apt/lists/*

Always:

  • Combine update and install in one RUN - running them separately caches the update and leads to stale package lists
  • Use --no-install-recommends to avoid pulling in unnecessary suggested packages
  • Clean up the package cache (rm -rf /var/lib/apt/lists/*) in the same layer

Alpine (apk)

RUN apk add --no-cache curl git ca-certificates

--no-cache tells apk not to store the package index - no separate cleanup step needed.

RHEL / CentOS (dnf / yum)

RUN dnf install -y curl git && \
    dnf clean all

Combining Commands

Every RUN is a separate layer. Combine related commands with && and \ for readability:

# 4 separate layers - cache issues, more image overhead
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN rm -rf /var/lib/apt/lists/*

#  One layer - clean, efficient
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        git && \
    rm -rf /var/lib/apt/lists/*

Why Cleanup Must Be in the Same Layer

This is a critical point that trips up many Dockerfiles:

# The cache directory is STILL in Layer 1 - Layer 2 can't remove it
RUN apt-get update && apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

#  Added and removed in the same layer - truly gone
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

Docker layers are immutable. If you add files in one RUN and delete them in a later RUN, the files still exist in the earlier layer - they're just hidden. The image size doesn't shrink.

Build and Cleanup in One Layer

For compiling from source or installing build tools you don't need at runtime:

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        gcc \
        make \
        libssl-dev && \
    ./configure && make && make install && \
    apt-get purge -y --auto-remove gcc make libssl-dev && \
    rm -rf /var/lib/apt/lists/*

Or better - use a multi-stage build so build tools never enter the final image at all (see the Multi-Stage Builds section).

Cache-Friendly RUN Ordering

RUN instructions that change rarely should come before those that change often. Once a layer is invalidated, all subsequent layers rebuild.

FROM python:3.12-slim

# ── Rarely changes - near the top ──────────────
RUN apt-get update && \
    apt-get install -y --no-install-recommends libpq5 && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app

# ── Changes when requirements.txt changes ──────
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ── Changes frequently - at the bottom ─────────
COPY . .
CMD ["python", "app.py"]
Exercise

Write an Efficient RUN Instruction

You want a single RUN instruction in a Dockerfile based on ubuntu:24.04 that updates apt, installs curl and wget without recommended packages, and cleans up the package lists afterward - all in one layer. Which instruction is correct?

Running Scripts

# Copy and run a setup script
COPY scripts/setup.sh /tmp/setup.sh
RUN chmod +x /tmp/setup.sh && /tmp/setup.sh && rm /tmp/setup.sh

# Or inline with heredoc (BuildKit / Docker 23+)
RUN <<EOF
  set -e
  apt-get update
  apt-get install -y --no-install-recommends curl
  rm -rf /var/lib/apt/lists/*
EOF

Heredoc syntax (<<EOF) is cleaner for multi-command blocks but requires BuildKit (enabled by default in Docker 23+).

Creating Users and Directories

# Create a non-root user and group
RUN groupadd -r appgroup && \
    useradd -r -g appgroup -d /app -s /sbin/nologin appuser

# Alpine equivalent
RUN addgroup -S appgroup && \
    adduser -S -G appgroup appuser

# Create directories with correct ownership
RUN mkdir -p /app/logs /app/tmp && \
    chown -R appuser:appgroup /app

Environment Variables in RUN

Shell form has access to environment variables set with ENV and ARG:

ARG NODE_VERSION=20
RUN echo "Installing Node $NODE_VERSION"

ENV APP_HOME=/app
RUN mkdir -p $APP_HOME && echo "Created $APP_HOME"

Exec form does NOT expand variables - use shell form or wrap with sh -c:

ENV APP_HOME=/app

# $APP_HOME is not expanded in exec form
RUN ["mkdir", "-p", "$APP_HOME"]

#  Shell form expands it
RUN mkdir -p $APP_HOME

#  Or explicit sh -c in exec form
RUN ["sh", "-c", "mkdir -p $APP_HOME"]

Caching RUN Instructions

Docker caches RUN layers based on the instruction string. If the string hasn't changed, the cached layer is reused - even if an external resource (like a package version) has changed.

To force a cache bust for package installs:

# Skip cache for this build
docker build --no-cache -t myapp .

# Or use a build arg to invalidate at a specific point
docker build --build-arg CACHE_BUST=$(date +%s) -t myapp .

In Dockerfile:

ARG CACHE_BUST=1
RUN apt-get update && apt-get upgrade -y  # Reruns when CACHE_BUST changes

Summary

  • RUN executes commands at build time - the result is baked into a layer
  • Combine related commands with && into a single RUN to reduce layers
  • Always clean up (caches, temp files, build tools) in the same RUN instruction
  • Use --no-install-recommends (apt) or --no-cache (apk) to keep installs lean
  • Shell form is standard for RUN; exec form avoids the shell but doesn't expand variables

Continue Learning

Explore Related Topics

Try the Tool

Related Resources