FROM and Base Images

The FROM instruction tells Docker what image your Dockerfile starts from. That choice matters because the base image supplies the filesystem, operating-system packages, language runtime, default metadata, and the first layers of your final image. A good base image makes builds smaller, safer, faster, and more reproducible; a careless one can make every container inherit surprises.

Overview: How FROM Works

Every Dockerfile must begin each build stage with FROM, except for parser directives and global ARG values that appear before it. Docker images are read-only stacks of layers. When you write FROM node:20-alpine, Docker resolves that name through a registry, pulls the image manifest if needed, downloads any missing layers, and then starts your build by adding new layers on top of the base image layers.

A base image is not special after the build begins. It is simply the lower part of the layer stack. If your Dockerfile adds RUN apk add curl, copies application files, and sets CMD, those instructions create new layers or metadata above the base. When you later run a container, Docker mounts the final read-only image layers and adds a thin writable container layer. Deleting the container removes that writable layer, not the image.

The base image name has three important parts: registry, repository, and tag or digest. In docker.io/library/nginx:1.27-alpine, docker.io is the registry, library/nginx is the repository, and 1.27-alpine is the tag. Docker lets you omit the registry and library/ namespace for Docker Hub official images, so nginx:1.27-alpine means the same common source. Tags are mutable labels. The maintainer can move node:20-alpine to a newer patch release. Digests, such as an image referenced with @sha256:..., identify exact content.

Base images range from full operating-system images such as ubuntu:24.04, to runtime images such as python:3.12-slim, to tiny images such as alpine:3.20, scratch, and distroless images. Smaller is often better, but not automatically. Alpine uses musl libc instead of glibc, which can affect native dependencies. scratch contains nothing at all, so it is useful mainly for static binaries or copying in every required file yourself.

Syntax

FROM [--platform=<platform>] <image>[:<tag>] [AS <stage-name>]
FROM [--platform=<platform>] <image>@<digest> [AS <stage-name>]
Part Meaning
FROM Starts a new build stage from a base image.
--platform Optionally selects an image variant such as linux/amd64 or linux/arm64. Use it only when you need a specific target architecture.
<image> The repository name, optionally including a registry and namespace, such as python or ghcr.io/example/api.
:<tag> A human-readable label for a version or variant. Prefer specific tags over latest.
@<digest> An immutable content identifier. This is the most reproducible form, though it requires maintenance when security updates are needed.
AS <stage-name> Names the stage so a later stage can copy files from it with COPY --from=<stage-name>.

You may use build arguments in FROM if the ARG appears before the first FROM. That is useful for controlled version selection, but avoid making every build float unpredictably.

Examples

Example 1: A Small Static Site

FROM nginx:1.27-alpine
COPY ./site /usr/share/nginx/html
EXPOSE 80

Output:

Step 1/3 : FROM nginx:1.27-alpine
1.27-alpine: Pulling from library/nginx
Status: Downloaded newer image for nginx:1.27-alpine
Step 2/3 : COPY ./site /usr/share/nginx/html
Step 3/3 : EXPOSE 80
Successfully built 7c8b5a2d4f10

This Dockerfile begins with the official Nginx image pinned to the 1.27-alpine variant, then copies local static files into the default Nginx web root. EXPOSE 80 records metadata saying the application listens on port 80, but it does not publish the port to the host. You still need docker run -p 8080:80 ... to reach it from your browser.

Example 2: Build and Run the Image

docker build -t static-site:1.0 .
docker run --rm -p 8080:80 static-site:1.0

Output:

[+] Building 4.2s (7/7) FINISHED
 => naming to docker.io/library/static-site:1.0
/docker-entrypoint.sh: Configuration complete; ready for start up

The build command creates a new local image named static-site:1.0. The run command creates a container from that image, adds a writable layer, starts the Nginx process defined by the base image, and publishes host port 8080 to container port 80. The application files are in an image layer, while runtime changes such as logs belong to the container layer or configured volumes.

Example 3: Multi-stage Node.js Build

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

Output:

Step 1/12 : FROM node:20-alpine AS builder
Step 6/12 : RUN npm run build
Step 7/12 : FROM node:20-alpine AS runtime
Step 11/12 : USER node
Step 12/12 : CMD ["node", "dist/server.js"]
Successfully built 34f65e9a00bf

The first FROM creates a builder stage with development dependencies available. The second FROM starts a fresh runtime stage and copies only the compiled dist directory from the builder. This keeps build tools and intermediate files out of the shipped image. Notice the dependency-friendly order: copy package manifests, install dependencies, then copy the rest of the source. Source changes do not invalidate the dependency install layer unless the manifest files change.

How It Works Step by Step

  1. Docker reads the Dockerfile from top to bottom. Parser directives and global ARG values may appear before FROM; normal instructions cannot.
  2. For each FROM, Docker resolves the image reference. If the local content store already has the required manifest and layers, it reuses them. Otherwise it asks the registry for the manifest and downloads missing layer blobs.
  3. Docker creates a build stage rooted at that base image. Later instructions in that stage create filesystem layers, update image configuration, or both.
  4. The build cache keys include the instruction text, relevant file contents, build arguments, and the parent layer. Changing the base image changes the parent and can invalidate everything after FROM.
  5. In a multi-stage build, each FROM starts over from a new base. Files move between stages only when you explicitly copy them with COPY --from=....
  6. At runtime, Docker does not rerun the Dockerfile. It uses the finished image metadata and layers, then adds a container writable layer and starts the configured process.

Common Mistakes

Using latest as a Production Base

FROM node:latest
COPY . /app

latest is a moving tag, not a promise of freshness, compatibility, or stability. A build today and a build next month can produce different images from the same Dockerfile. Use a specific supported tag, and use digest pinning when you need byte-for-byte reproducibility.

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

Choosing a Base Image by Size Alone

Very small images can be excellent, but they also remove tools you may assume exist. Alpine can expose native package differences, and scratch has no shell, CA certificates, package manager, or users. Pick the smallest image that still matches your runtime and operational needs. For many production applications, slim, Alpine, or a distroless runtime image is a better choice than a full general-purpose distribution.

Baking Secrets into a Base Layer

FROM ubuntu:24.04
ENV API_TOKEN="<YOUR_API_TOKEN>"
RUN echo "$API_TOKEN" > /tmp/token.txt
RUN rm /tmp/token.txt

This is still unsafe. Docker layers are immutable history; removing a file in a later layer does not erase it from an earlier layer. Secrets should come from runtime environment variables, Docker secrets, mounted files, or your orchestrator’s secret store, not from ENV or RUN instructions in an image.

Best Practices

  • Use specific, supported base image tags such as python:3.12-slim, node:20-alpine, or nginx:1.27-alpine instead of bare image names.
  • Consider digest pinning for production supply-chain control, then schedule regular updates so security fixes are not frozen forever.
  • Use official or trusted images, and understand who maintains the repository you are inheriting from.
  • Use multi-stage builds to keep compilers, package caches, source maps, and test tools out of the runtime image.
  • Put stable dependency files before frequently changing source files to preserve the build cache.
  • Prefer non-root runtime users when the base image provides one, or create one explicitly.
  • Use .dockerignore so your build context does not include node_modules, local credentials, Git history, logs, or build artifacts.
  • Do not assume EXPOSE publishes a port. Publish with docker run -p or Compose ports:.
  • Test on the same CPU architecture you deploy to, or intentionally build multi-platform images when your users run both ARM and x86 machines.

Practice Exercises

  1. Create a Dockerfile for a Python web app using python:3.12-slim. Put dependency installation before copying the full source tree. Expected end state: editing an application file should not rerun dependency installation.
  2. Convert a single-stage Node.js Dockerfile into a two-stage build with a named builder stage and a smaller runtime stage. Hint: copy only compiled output and production dependencies into the final image.
  3. Inspect three possible base images for the same app: a full distribution image, a slim runtime image, and an Alpine variant. Write down the tradeoff between image size, compatibility, available debugging tools, and patch process.

Summary

  • FROM starts a build stage and defines the lower layers your image inherits.
  • Tags are convenient but mutable; specific tags are better than latest, and digests are the most reproducible.
  • Each FROM in a multi-stage Dockerfile starts a separate stage.
  • Base image changes affect cache reuse because every following layer depends on the parent layer.
  • The right base image balances size, compatibility, security maintenance, and operational needs.