DevOpsLesson
DevOpsLesson

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Stay Updated

Get notified about new tutorials and features.

Tutorials

  • What is DevOps?
  • Docker Tutorial
  • Terraform Tutorial
  • CI/CD Pipeline
  • All Tutorials

Roadmaps

  • DevOps Engineer
  • Cloud Engineer
  • SRE Path
  • All Roadmaps

Company

  • About Us
  • Blog
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 DevOpsLesson. All rights reserved.

DOCKERKUBERNETESTERRAFORMAWSCI/CDLINUXGITDEVOPS ROADMAPCLOUD ROADMAPSRE ROADMAPGIT CHEATSHEETDOCKER CHEATSHEETK8S CHEATSHEETTF CHEATSHEETLINUX CHEATSHEETDOCKERFILE LINTERYAML VALIDATORCRON PARSERREGEX TESTER

Docker Tutorial

Introduction to docker
Why Use Docker?
Docker vs Virtual Machines
Installing Docker
Key Docker Concepts
Docker Images
Docker Containers
Writing Dockerfiles
Docker Volumes

RUN

PreviousPrev
Next

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
PreviousPrev
Next

Continue Learning

Writing Dockerfiles

Learn to write Dockerfiles from scratch - every instruction explained, with patterns and pitfalls from real-world usage.

35 min·Easy

FROM

The FROM instruction starts every Dockerfile. Learn how to choose the right base image, use variants wisely, and unlock multi-stage builds.

8 min·Easy

COPY & ADD

Copy files into your image correctly. Learn the difference between COPY and ADD, how to set ownership, use .dockerignore, and avoid common mistakes.

7 min·Easy

Explore Related Topics

Ku

Kubernetes Tutorials

Orchestrate your Docker containers at scale

CI

CI/CD Tutorials

Automate Docker builds and deployments in pipelines

Try the Tool

Dockerfile Linter

Instantly lint your Dockerfile for best-practice violations and security issues.

On This Page

RUNBasic SyntaxInstalling PackagesCombining CommandsWhy Cleanup Must Be in the Same LayerBuild and Cleanup in One LayerCache-Friendly RUN OrderingRunning ScriptsCreating Users and DirectoriesEnvironment Variables in RUNCaching `RUN` InstructionsSummary