FROM
The FROM instruction starts every Dockerfile. Learn how to choose the right base image, use variants wisely, and unlock multi-stage builds.
FROM
Every Dockerfile begins with FROM. It sets the foundation that all subsequent instructions build on - the base image your application inherits its OS, runtime, and system libraries from. Choosing well here determines your image size, security surface, and compatibility.
Basic Syntax
FROM <image>
FROM <image>:<tag>
FROM <image>@<digest>
FROM <image>:<tag> AS <name>
Choosing a Base Image
The most impactful decision in any Dockerfile. The wrong choice can mean a 1GB image when 20MB would do.
Official images first
Always start with official images from Docker Hub - they're maintained, regularly patched, and well-documented:
FROM node:20 # Official Node.js
FROM python:3.12 # Official Python
FROM golang:1.22 # Official Go
FROM postgres:16 # Official PostgreSQL
FROM nginx:1.25 # Official Nginx
FROM ubuntu:24.04 # Official Ubuntu
Pick the right variant
Most official images ship in multiple variants. The tag tells you which:
# Full image - everything included, largest size
FROM node:20
# Slim - trimmed system packages, good balance
FROM node:20-slim
# Alpine - musl libc, minimal footprint (~5MB base)
FROM node:20-alpine
# Specific Debian release
FROM node:20-bookworm
FROM node:20-bookworm-slim
Size comparison for Node.js:
| Tag | Approx. Size | Notes |
|---|---|---|
node:20 | ~1.1GB | Full Debian, all tools |
node:20-slim | ~240MB | Debian, fewer packages |
node:20-alpine | ~135MB | Alpine Linux |
node:20-alpine + multi-stage | ~80MB | Only production deps |
For production workloads, start with -alpine unless you hit compatibility issues. Some packages with native bindings (e.g. bcrypt, sharp) need glibc and won't work on Alpine without workarounds - use -slim in those cases.
Pin to a specific version
Avoid latest in production - it's a moving target that can break your build unexpectedly:
# Unpredictable - could change tomorrow
FROM node:latest
FROM node:20
# Predictable - this exact release forever
FROM node:20.11.0
FROM node:20.11.0-alpine3.19
# Most immutable - pinned to a digest
FROM node:20-alpine@sha256:bf77dc26e48ea95fca9d1aceb5acfa69d2e546b765ec2abfb502975f1a2d4def
For CI/CD pipelines and production images, pin to a full version tag. Reserve latest for local experimentation.
FROM scratch - The Empty Image
scratch is a special pseudo-image meaning "start with nothing":
FROM scratch
COPY myapp /myapp
CMD ["/myapp"]
This is only useful for statically compiled binaries (Go, Rust, C). The result is the smallest possible image - just your binary.
# Go multi-stage example using scratch
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .
FROM scratch
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
# Final image: ~8MB (just the binary)
Multi-Stage FROM
Use multiple FROM statements to separate build and runtime environments. Each FROM starts a new stage:
# Stage 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: production (only this stage ships)
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
Naming stages with AS <name> makes COPY --from=<name> readable. You can also reference stages by number (0-indexed), but names are clearer.
Copying From External Images
COPY --from isn't limited to your own stages - you can pull files from any public image:
FROM alpine
# Copy the nginx binary from the official nginx image
COPY --from=nginx:alpine /usr/sbin/nginx /usr/sbin/nginx
This is useful for including a specific tool without making it a full base image dependency.
Platform Targeting
Build images for a specific CPU architecture:
# Explicitly target AMD64 (useful on Apple Silicon Macs)
FROM --platform=linux/amd64 node:20-alpine
# Target ARM64 for Raspberry Pi or AWS Graviton
FROM --platform=linux/arm64 node:20-alpine
Or build multi-platform images with docker buildx:
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .
Compare Base Image Sizes
You want to pull both node:20 and node:20-alpine, then compare their sizes side by side. Which sequence of commands is correct?
Keeping Base Images Updated
Even a pinned base image can have vulnerabilities. Rebuild regularly:
# Force pull the latest version of the pinned tag
docker build --pull -t myapp:latest .
# Or update the digest in your Dockerfile periodically
docker pull node:20-alpine
docker inspect node:20-alpine --format='{{index .RepoDigests 0}}'
# → node:20-alpine@sha256:bf77dc26...
In CI, run docker build --pull on a schedule (e.g. weekly) to pick up base image security patches.
Summary
FROMsets the entire foundation - it's the most impactful line in your Dockerfile- Use official images; prefer
-alpinefor size,-slimif alpine causes compatibility issues - Pin to a specific version tag (or digest) in production - never use
latest FROM scratchis for statically compiled binaries only- Multi-stage
FROMlets you separate build tools from the runtime image - always worth it for compiled languages