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
- Docker reads the Dockerfile from top to bottom. Parser directives and global
ARGvalues may appear beforeFROM; normal instructions cannot. - 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. - Docker creates a build stage rooted at that base image. Later instructions in that stage create filesystem layers, update image configuration, or both.
- 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. - In a multi-stage build, each
FROMstarts over from a new base. Files move between stages only when you explicitly copy them withCOPY --from=.... - 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, ornginx:1.27-alpineinstead 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
.dockerignoreso your build context does not includenode_modules, local credentials, Git history, logs, or build artifacts. - Do not assume
EXPOSEpublishes a port. Publish withdocker run -por Composeports:. - 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
- 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. - Convert a single-stage Node.js Dockerfile into a two-stage build with a named
builderstage and a smallerruntimestage. Hint: copy only compiled output and production dependencies into the final image. - 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
FROMstarts 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
FROMin 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.
