Skip to main content

Create Your First Container Image

A container image is an immutable, layer-based snapshot of an application with all its dependencies. This guide takes you from zero to a production-ready image.


Core Concepts

TermMeaning
ImageImmutable snapshot (read-only layer stack)
ContainerRunning instance of an image
DockerfileBuild instructions for an image
RegistryImage repository (Docker Hub, GHCR, Harbor, ...)
LayerEach Dockerfile instruction creates a cached layer
Dockerfile  ──docker build──►  Image  ──docker run──►  Container

docker push

Registry

Install Docker (Linux)

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER # Use without sudo (re-login required!)
docker version
docker info

The First Dockerfile

Minimal Example — Node.js App

# syntax=docker/dockerfile:1

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Runtime (leaner final image)
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .

ENV NODE_ENV=production
EXPOSE 3000
USER node # Don't run as root!

CMD ["node", "server.js"]

Dockerfile Instructions Overview

InstructionPurpose
FROMBase image (required, always first)
WORKDIRSet working directory
COPYCopy files from host into image
ADDLike COPY but also supports URLs & tar extraction
RUNExecute command during build
ENVSet environment variables
EXPOSEDocument a port (informational, no firewall effect)
USERUser for subsequent instructions
CMDDefault start command (overridable)
ENTRYPOINTFixed entry point (CMD passed as arguments)
ARGBuild-time variable (--build-arg)
HEALTHCHECKDefine container health status

Building an Image

# Basic build
docker build -t myapp:1.0.0 .

# With build argument
docker build --build-arg APP_ENV=production -t myapp:prod .

# Skip cache
docker build --no-cache -t myapp:latest .

# Multi-platform (e.g. ARM + AMD64)
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .

Starting & Managing Containers

# Start a container
docker run -d -p 8080:3000 --name myapp myapp:1.0.0

# With environment variables & volume
docker run -d \
-p 8080:3000 \
-e DATABASE_URL=postgres://... \
-v /opt/app/data:/app/data \
--restart unless-stopped \
--name myapp \
myapp:1.0.0

# Logs
docker logs -f myapp

# Shell inside running container
docker exec -it myapp sh

# Container status
docker ps -a
docker stats

# Cleanup
docker stop myapp && docker rm myapp
docker image prune -a # Remove unused images

.dockerignore

Prevents unnecessary files from being copied into the build context (faster builds, smaller images):

.git
node_modules
dist
*.log
.env
.env.*
README.md
tests/

Best Practices for Production-Ready Images

1. Use Multi-Stage Builds

Separate build dependencies from runtime to keep the final image small.

2. Choose a Minimal Base Image

FROM alpine:3.19          # ~7 MB
FROM debian:bookworm-slim # ~75 MB
FROM scratch # Absolutely empty (for static binaries only)

3. Optimize Layer Caching

# BAD — dependencies reinstalled on every code change
COPY . .
RUN npm ci

# GOOD — dependencies cached as long as package.json is unchanged
COPY package*.json ./
RUN npm ci
COPY . .

4. Never Run as Root

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

5. Never Store Secrets in the Image

# NEVER do this:
ENV DATABASE_PASSWORD=supersecret # Ends up in the image layer!

# Instead: pass at runtime
docker run -e DATABASE_PASSWORD=$DB_PASS myapp
# Or: Docker Secrets / Kubernetes Secrets

6. Define a HEALTHCHECK

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1

Analyzing Image Size

docker image inspect myapp:1.0.0
docker history myapp:1.0.0 # Show layer sizes
dive myapp:1.0.0 # Interactive layer explorer (tool: dive)

Pushing an Image to a Registry

# Docker Hub
docker login
docker tag myapp:1.0.0 username/myapp:1.0.0
docker push username/myapp:1.0.0

# GitHub Container Registry (GHCR)
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
docker tag myapp:1.0.0 ghcr.io/org/myapp:1.0.0
docker push ghcr.io/org/myapp:1.0.0