When newcomers encounter Docker images named after operating systems (like ubuntu
or alpine
), it's natural to assume they contain full OS installations. The key distinction is that these images package user-space components without duplicating kernel resources.
A Docker image tagged as ubuntu:22.04
contains:
- Minimal root filesystem with Ubuntu's directory structure
- Essential binaries like
apt
,bash
, and core utilities - Configuration files maintaining Ubuntu conventions
- Package manager metadata (but no actual kernel packages)
When you execute:
docker run -it ubuntu /bin/bash
The container:
- Creates an isolated filesystem using Ubuntu's rootfs
- Utilizes the host's kernel through namespaces and cgroups
- Mounts the image layers as a union filesystem
Compare these commands on your host machine:
# Host kernel version
uname -r
# Container's kernel version (will match host)
docker run --rm alpine uname -r
# Container's user-space tools
docker run --rm alpine cat /etc/os-release
Image | Size | Equivalent VM |
---|---|---|
alpine:latest | 5.6MB | ~500MB (minimal) |
ubuntu:22.04 | 72.8MB | ~2.5GB (standard) |
centos:7 | 204MB | ~4GB (minimal) |
Here's how to build a minimal Python environment:
FROM alpine:3.18
RUN apk add --no-cache python3 py3-pip
COPY requirements.txt .
RUN pip install -r requirements.txt
CMD ["python3"]
Containers fall short when you need:
- Different kernel versions (e.g., testing RHEL8 features on Ubuntu host)
- Custom kernel modules
- Full systemd initialization
- Hardware virtualization (GPUs often need special passthrough)
This technique demonstrates true "OS-less" containers:
# Build stage with full OS
FROM golang:1.20 as builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# Runtime stage with just binary
FROM scratch
COPY --from=builder /app/myapp /
CMD ["/myapp"]
The final image contains only your compiled binary - no OS files whatsoever.
Because containers share the host kernel:
- Kernel vulnerabilities affect all containers
- Container escape exploits are more dangerous than VM escapes
- Seccomp and AppArmor profiles become critical
Many developers new to Docker experience cognitive dissonance when encountering OS-named images like ubuntu
or debian
. The confusion stems from two seemingly contradictory facts:
- Containers share the host OS kernel (no guest OS virtualization)
- We use base images named after operating systems
An OS-named Docker image doesn't contain a full operating system kernel. Instead, it packages:
# Breakdown of a typical ubuntu:22.04 Docker image
/bin # Core utilities (bash, ls, etc.)
/lib # Shared libraries
/usr # Additional programs and docs
/etc # Configuration files
/var # Variable data
The key difference from a VM becomes apparent when examining process hierarchy:
# Host machine (run on Linux host)
ps auxf
├─dockerd
├─containerd
├─containerd-shim
├─nginx # Container process appears directly in host process tree
When you run:
docker run -it ubuntu /bin/bash
The container:
- Uses the host's Linux kernel
- Mounts the Ubuntu image's filesystem as its root
- Creates isolated namespaces for processes, network, etc.
Characteristic | Docker Container | Virtual Machine |
---|---|---|
Boot Time | Milliseconds (no kernel init) | Seconds (full OS boot) |
Memory Overhead | MBs (just app processes) | GBs (entire OS footprint) |
Kernel Access | Direct host kernel syscalls | Virtualized through hypervisor |
Consider this multi-stage build example:
# Stage 1: Build with full OS image
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y build-essential
COPY . /app
WORKDIR /app
RUN make
# Stage 2: Runtime with minimal base
FROM alpine:3.16
COPY --from=builder /app/output /usr/local/bin
CMD ["/usr/local/bin/myapp"]
Notice how we:
- Use Ubuntu for building (rich toolchain available)
- Switch to Alpine for runtime (minimal dependencies)
- Never duplicate kernel components
While containers share the host kernel, the base image affects:
# Different glibc versions between CentOS and Ubuntu
docker run centos:7 ldd --version
# vs.
docker run ubuntu:22.04 ldd --version
This explains why some applications behave differently across base images despite using the same host kernel.
For maximum minimalism:
FROM scratch
COPY hello /
CMD ["/hello"]
This container has no OS files - just your static binary and the host kernel.