Overview
Container images package applications and their dependencies into immutable, portable artifacts that run consistently across different computing environments. An image contains everything needed to execute an application: code, runtime, system tools, libraries, and configuration files. The image serves as a template for creating container instances.
Images function as read-only templates stored in layers. Each layer represents a filesystem change, such as installing packages or copying files. The container runtime stacks these layers to create a unified filesystem view. When a container runs from an image, the runtime adds a writable layer on top where the application makes runtime changes.
The layered architecture enables efficient storage and distribution. Multiple images sharing common base layers store only the unique layers, reducing disk usage and network transfer time. A registry stores images in a format optimized for layer deduplication.
Container images follow the Open Container Initiative (OCI) Image Format Specification, which defines the structure and metadata format. This standardization allows images to run on different container runtimes like Docker, containerd, and Podman without modification.
# Example: Pulling and inspecting an image
require 'open3'
def inspect_image(image_name)
stdout, stderr, status = Open3.capture3("docker image inspect #{image_name}")
if status.success?
JSON.parse(stdout).first
else
raise "Failed to inspect image: #{stderr}"
end
end
image_info = inspect_image('ruby:3.2-alpine')
puts "Image ID: #{image_info['Id']}"
puts "Created: #{image_info['Created']}"
puts "Size: #{image_info['Size']} bytes"
Key Principles
Image Immutability
Images remain unchanged after creation. Building a new image version with modifications creates a distinct image with a unique identifier. This immutability guarantees reproducibility: an image produces identical containers regardless of when or where it runs. Applications cannot modify the image layers; runtime changes occur only in the ephemeral writable layer.
Layer Composition
Each instruction in a Dockerfile creates a layer. The FROM instruction establishes the base layer, subsequent instructions add layers on top. The container runtime uses a union filesystem to merge layers into a single coherent filesystem. Layers stack in order: later layers override files from earlier layers.
Layer caching accelerates builds. When rebuilding an image, the build system reuses cached layers if the instruction and context match previous builds. Changing an instruction invalidates that layer's cache and all subsequent layers.
Image Identification
Three mechanisms identify images: tags, digests, and image IDs. Tags provide human-readable names like ruby:3.2 or myapp:v1.0.0. A single image can have multiple tags. Digests use SHA256 hashes of the image manifest, providing cryptographic verification. Image IDs uniquely identify images locally using the first 12 characters of the configuration hash.
# Example showing different image identifiers
# By tag
docker pull ruby:3.2
# By digest (immutable reference)
docker pull ruby@sha256:a1b2c3d4e5f6...
# By image ID (local reference)
docker images --digests
Registry Architecture
Container registries store and distribute images. A registry organizes images into repositories, which contain multiple versions of related images. The registry API handles push and pull operations, authentication, and image metadata queries. Popular registries include Docker Hub, GitHub Container Registry, and private registries like Harbor or AWS ECR.
The pull operation downloads only missing or updated layers. When pulling ruby:3.2, the registry transmits each layer as a compressed archive. The local daemon extracts and caches layers by their content hash, enabling efficient layer sharing across images.
Manifest Structure
An image manifest describes the image configuration and layers. The manifest lists each layer's digest, size, and media type. Multi-platform images use a manifest list (or OCI index) that references platform-specific manifests for different architectures like amd64, arm64, or s390x.
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 7023,
"digest": "sha256:b5b2d2d61e..."
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2811969,
"digest": "sha256:c9b1b535fa..."
}
]
}
Ruby Implementation
Ruby applications containerize using official Ruby base images or custom images built on Alpine or Ubuntu. The build process installs dependencies, copies application code, and configures the runtime environment.
Base Image Selection
Official Ruby images come in three variants: full Debian-based images (ruby:3.2), slim versions with reduced packages (ruby:3.2-slim), and Alpine Linux images (ruby:3.2-alpine). Debian images include build tools and common libraries, making them suitable for development. Slim images remove documentation and non-essential packages, reducing size by 40-50%. Alpine images produce the smallest final size but use musl libc instead of glibc, occasionally causing compatibility issues with native extensions.
# Full image - includes build tools, git, etc.
FROM ruby:3.2
# Slim image - minimal Ruby runtime
FROM ruby:3.2-slim
# Alpine image - smallest size, different C library
FROM ruby:3.2-alpine
Bundler Integration
Container builds install gems using Bundler. Separating dependency installation from code copying improves layer caching. Changes to application code do not invalidate the gem installation layer, speeding up rebuilds.
FROM ruby:3.2-slim
WORKDIR /app
# Copy only Gemfile and Gemfile.lock first
COPY Gemfile Gemfile.lock ./
# Install dependencies in a separate layer
RUN bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install
# Copy application code
COPY . .
# Set Rails environment
ENV RAILS_ENV=production
# Precompile assets for Rails applications
RUN bundle exec rails assets:precompile
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
Multi-Stage Builds for Ruby
Multi-stage builds separate build-time dependencies from runtime dependencies. The first stage compiles native extensions and precompiles assets. The final stage copies only the compiled artifacts and runtime dependencies, producing a smaller image.
# Build stage
FROM ruby:3.2-slim AS builder
RUN apt-get update && \
apt-get install -y build-essential libpq-dev nodejs && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install
COPY . .
RUN bundle exec rails assets:precompile
# Runtime stage
FROM ruby:3.2-slim
RUN apt-get update && \
apt-get install -y libpq5 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy installed gems from builder
COPY --from=builder /usr/local/bundle /usr/local/bundle
# Copy application and compiled assets
COPY --from=builder /app /app
ENV RAILS_ENV=production
EXPOSE 3000
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
Native Extension Handling
Ruby gems with native extensions require C compilers and development headers during installation. The build stage installs these dependencies, compiles the extensions, then the runtime stage omits build tools. Common extensions requiring compilation include pg (PostgreSQL client), nokogiri (XML/HTML parser), and sassc (Sass compiler).
# Alpine-based build requiring additional packages for native gems
FROM ruby:3.2-alpine AS builder
RUN apk add --no-cache \
build-base \
postgresql-dev \
libxml2-dev \
libxslt-dev
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle install
# Runtime needs only runtime libraries, not build tools
FROM ruby:3.2-alpine
RUN apk add --no-cache \
postgresql-client \
libxml2 \
libxslt \
tzdata
WORKDIR /app
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY . .
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Docker Compose for Ruby Development
Development environments use Docker Compose to orchestrate multiple services. A typical Ruby application stack includes the web application, PostgreSQL database, Redis cache, and Sidekiq worker.
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile.dev
command: bundle exec rails server -b 0.0.0.0
volumes:
- .:/app
- bundle_cache:/usr/local/bundle
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@db/myapp_development
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=password
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
sidekiq:
build:
context: .
dockerfile: Dockerfile.dev
command: bundle exec sidekiq
volumes:
- .:/app
- bundle_cache:/usr/local/bundle
environment:
- DATABASE_URL=postgresql://postgres:password@db/myapp_development
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
volumes:
postgres_data:
redis_data:
bundle_cache:
Practical Examples
Basic Sinatra Application
A minimal Sinatra application demonstrates the fundamental image build process. The Dockerfile installs dependencies, copies code, and specifies the startup command.
FROM ruby:3.2-slim
WORKDIR /app
# Install dependencies
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle install
# Copy application
COPY app.rb config.ru ./
EXPOSE 4567
CMD ["bundle", "exec", "rackup", "-o", "0.0.0.0", "-p", "4567"]
# app.rb
require 'sinatra'
get '/' do
"Container running Ruby #{RUBY_VERSION}"
end
get '/health' do
status 200
content_type :json
{ status: 'healthy', timestamp: Time.now.iso8601 }.to_json
end
Build and run commands:
docker build -t sinatra-app:v1 .
docker run -p 4567:4567 sinatra-app:v1
Rails Application with Asset Pipeline
Rails applications require additional build steps for asset compilation. The Dockerfile installs Node.js for JavaScript processing, precompiles assets, and cleans up build artifacts.
FROM ruby:3.2-slim AS builder
# Install Node.js and build dependencies
RUN apt-get update && \
apt-get install -y \
build-essential \
libpq-dev \
nodejs \
npm && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install dependencies
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install
COPY package.json package-lock.json ./
RUN npm ci
# Copy application
COPY . .
# Precompile assets
ENV RAILS_ENV=production
ENV SECRET_KEY_BASE=dummy_key_for_asset_compilation
RUN bundle exec rails assets:precompile && \
rm -rf node_modules tmp/cache
# Runtime stage
FROM ruby:3.2-slim
RUN apt-get update && \
apt-get install -y libpq5 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY --from=builder /app /app
RUN useradd -m -u 1000 rails && \
chown -R rails:rails /app
USER rails
ENV RAILS_ENV=production
ENV RAILS_LOG_TO_STDOUT=true
ENV RAILS_SERVE_STATIC_FILES=true
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Automated Testing in Containers
Test suites run in containers to ensure consistent test environments. A dedicated test Dockerfile includes development dependencies and test frameworks.
# Dockerfile.test
FROM ruby:3.2-slim
RUN apt-get update && \
apt-get install -y \
build-essential \
libpq-dev \
chromium \
chromium-driver && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle install
COPY . .
ENV RAILS_ENV=test
ENV DATABASE_URL=postgresql://postgres:password@db/myapp_test
CMD ["bundle", "exec", "rspec"]
# docker-compose.test.yml
version: '3.8'
services:
test:
build:
context: .
dockerfile: Dockerfile.test
environment:
- DATABASE_URL=postgresql://postgres:password@test-db/myapp_test
depends_on:
- test-db
volumes:
- ./spec:/app/spec
test-db:
image: postgres:15-alpine
environment:
- POSTGRES_PASSWORD=password
tmpfs:
- /var/lib/postgresql/data
Run tests:
docker-compose -f docker-compose.test.yml run --rm test
Microservices Architecture
A microservices setup creates separate images for different service responsibilities. Each service has its own Dockerfile and dependencies.
# services/api/Dockerfile
FROM ruby:3.2-alpine
WORKDIR /app
RUN apk add --no-cache postgresql-client
COPY services/api/Gemfile services/api/Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle install
COPY services/api ./
COPY lib ./lib
CMD ["bundle", "exec", "puma"]
# services/worker/Dockerfile
FROM ruby:3.2-alpine
WORKDIR /app
RUN apk add --no-cache postgresql-client imagemagick
COPY services/worker/Gemfile services/worker/Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle install
COPY services/worker ./
COPY lib ./lib
CMD ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"]
Implementation Approaches
Dockerfile-Based Builds
Dockerfile-based builds use declarative instructions to construct images. Each instruction executes in sequence, creating layers. The approach provides transparency: the Dockerfile serves as documentation showing exactly how the image builds.
Order instructions to maximize cache efficiency. Place instructions that change infrequently near the beginning. Install system packages first, then language dependencies, finally application code. Layer invalidation cascades: changing an instruction invalidates its cache and all subsequent layers.
# Optimized layer ordering
FROM ruby:3.2-slim
# System packages change rarely - install first
RUN apt-get update && \
apt-get install -y libpq5 && \
rm -rf /var/lib/apt/lists/*
# Dependencies change occasionally
COPY Gemfile Gemfile.lock ./
RUN bundle install
# Application code changes frequently - copy last
COPY . .
Buildpacks
Cloud Native Buildpacks detect application types and create images without Dockerfiles. The buildpack inspects the project directory, identifies the language and framework, then applies appropriate build logic. Heroku and Cloud Foundry popularized this approach.
Buildpacks standardize image creation across organizations. Developers push code without maintaining Dockerfiles. Platform teams manage buildpack configurations centrally, ensuring consistent security patches and runtime versions.
# Build Ruby application using Paketo buildpack
pack build myapp --builder paketobuildpacks/builder:base
# Buildpack detects Gemfile, installs dependencies, configures runtime
The buildpack approach trades flexibility for convenience. Custom build requirements may need Dockerfile-based builds or buildpack customization.
Builder Pattern
The builder pattern separates build-time and runtime concerns. A builder image contains compilers, build tools, and development headers. After building artifacts, the pattern copies only runtime necessities to a minimal final image.
# Builder image with all build dependencies
FROM ruby:3.2 AS builder
RUN apt-get update && \
apt-get install -y build-essential libpq-dev && \
rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle install --jobs 4
COPY . .
RUN bundle exec rails assets:precompile
# Minimal runtime image
FROM ruby:3.2-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY --from=builder /build /app
EXPOSE 3000
CMD ["bundle", "exec", "puma"]
Builder images often exceed 1GB while final runtime images stay under 300MB. The size reduction improves deployment speed and reduces attack surface.
Scripted Builds
Complex build processes use scripts that invoke the container runtime programmatically. Scripts handle dynamic tagging, multi-platform builds, registry authentication, and build matrix generation.
#!/usr/bin/env ruby
# build.rb
require 'json'
require 'open3'
class ImageBuilder
def initialize(app_name, version)
@app_name = app_name
@version = version
@registry = ENV['REGISTRY'] || 'docker.io'
end
def build
tags = generate_tags
platforms = %w[linux/amd64 linux/arm64]
tags.each do |tag|
build_multiplatform(tag, platforms)
end
end
private
def generate_tags
[
"#{@registry}/#{@app_name}:#{@version}",
"#{@registry}/#{@app_name}:latest"
]
end
def build_multiplatform(tag, platforms)
cmd = [
'docker', 'buildx', 'build',
'--platform', platforms.join(','),
'--tag', tag,
'--push',
'.'
]
stdout, stderr, status = Open3.capture3(*cmd)
unless status.success?
raise "Build failed: #{stderr}"
end
puts "Built and pushed: #{tag}"
end
end
builder = ImageBuilder.new('myapp', ENV['VERSION'] || 'dev')
builder.build
Security Implications
Base Image Vulnerabilities
Base images contain operating system packages with known vulnerabilities. Older base image versions accumulate security issues over time. Regularly rebuild images using updated base images to receive security patches.
Use minimal base images to reduce vulnerability exposure. Alpine Linux images contain fewer packages than Debian-based images, decreasing the number of potential vulnerabilities. Scan images for known CVEs using tools like Trivy or Grype.
# Scan image for vulnerabilities
trivy image ruby:3.2-alpine
# Scan with severity threshold
trivy image --severity HIGH,CRITICAL myapp:latest
Secrets Management
Never embed secrets directly in images. Build arguments and environment variables set during build time persist in image layers. An attacker with image access can extract these values from layer metadata.
# INSECURE - secret embedded in layer
FROM ruby:3.2-slim
ENV DATABASE_PASSWORD=secret123
# SECURE - secret provided at runtime
FROM ruby:3.2-slim
# Password comes from environment variable at container start
Mount secrets at runtime using environment variables, secret management systems like HashiCorp Vault, or Docker secrets in Swarm mode. Kubernetes uses Secret objects mounted as files or environment variables.
User Permissions
Container processes default to running as root. An attacker exploiting an application vulnerability gains root access within the container. Create dedicated users with minimal permissions.
FROM ruby:3.2-slim
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
# Create non-root user
RUN useradd -m -u 1001 appuser && \
chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
CMD ["bundle", "exec", "puma"]
Some operations require root privileges during build but not runtime. Install packages and configure system settings as root, then switch to a non-root user for the final command.
Supply Chain Security
Verify base image authenticity using content trust and signature verification. Docker Content Trust (DCT) uses The Update Framework (TUF) to sign and verify images. Enable DCT to prevent pulling tampered images.
export DOCKER_CONTENT_TRUST=1
docker pull ruby:3.2-slim
# Verifies signatures before pulling
Pin base images to specific digests rather than mutable tags. Tags like ruby:3.2 move to new versions containing different content. Digest references guarantee immutability.
# Mutable tag - content changes over time
FROM ruby:3.2-slim
# Immutable digest - always references the same image
FROM ruby@sha256:a7b8c9d0e1f2a3b4c5d6e7f8g9h0i1j2k3l4m5n6o7p8q9r0s1t2u3v4w5x6y7z8
Image Scanning in CI/CD
Integrate vulnerability scanning into continuous integration pipelines. Fail builds when critical vulnerabilities appear. Configure exceptions for accepted risks with compensating controls.
# .github/workflows/build.yml
name: Build and Scan
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
severity: 'CRITICAL,HIGH'
exit-code: '1' # Fail build on findings
Tools & Ecosystem
Docker
Docker provides the container runtime, build system, and image management tools. The Docker daemon manages images, containers, networks, and volumes. The Docker CLI communicates with the daemon to execute operations.
# Build image
docker build -t myapp:v1 .
# List images
docker images
# Remove image
docker rmi myapp:v1
# Save image to tar archive
docker save myapp:v1 -o myapp-v1.tar
# Load image from archive
docker load -i myapp-v1.tar
# View image history
docker history myapp:v1
# Prune unused images
docker image prune -a
Buildx
Buildx extends Docker build capabilities with BuildKit backend support. BuildKit improves build performance through parallel layer building, incremental layer transfers, and advanced caching.
# Create buildx builder
docker buildx create --name mybuilder --use
# Build multi-platform image
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag myapp:v1 \
--push \
.
# Build with cache from registry
docker buildx build \
--cache-from type=registry,ref=myapp:cache \
--cache-to type=registry,ref=myapp:cache \
--tag myapp:v1 \
.
Podman
Podman provides a daemonless container engine compatible with Docker images and Dockerfiles. Podman runs without a background daemon, executing containers as child processes. This architecture eliminates the single point of failure inherent in Docker's daemon design.
# Build image (identical to Docker)
podman build -t myapp:v1 .
# Generate Kubernetes YAML from image
podman generate kube myapp:v1 > deployment.yaml
# Build using Containerfile (Dockerfile equivalent)
podman build -f Containerfile -t myapp:v1 .
Container Registries
Registries store and distribute images. Public registries like Docker Hub host open-source images. Private registries protect proprietary images with authentication.
Docker Hub configuration:
# Login to Docker Hub
docker login
# Push image
docker push username/myapp:v1
# Pull image
docker pull username/myapp:v1
GitHub Container Registry (ghcr.io):
# Login using personal access token
echo $GITHUB_TOKEN | docker login ghcr.io -u username --password-stdin
# Push image
docker push ghcr.io/username/myapp:v1
AWS Elastic Container Registry (ECR):
# Authenticate to ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
123456789012.dkr.ecr.us-east-1.amazonaws.com
# Push image
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:v1
Harbor (self-hosted registry):
Harbor adds vulnerability scanning, image signing, and replication policies. Deploy Harbor in Kubernetes or on virtual machines.
Skopeo
Skopeo performs image operations without requiring a container runtime. Copy images between registries, inspect remote images, and delete tags programmatically.
# Copy image between registries
skopeo copy \
docker://docker.io/myapp:v1 \
docker://ghcr.io/username/myapp:v1
# Inspect remote image without pulling
skopeo inspect docker://ruby:3.2-alpine
# List tags in repository
skopeo list-tags docker://docker.io/library/ruby
# Delete image from registry
skopeo delete docker://myapp:old-version
Dive
Dive analyzes image layers, showing file changes in each layer. Identify unnecessary files increasing image size.
# Analyze image
dive myapp:v1
# Shows:
# - Layer commands
# - Files added/removed in each layer
# - Total layer size
# - Image efficiency score
Container Structure Tests
Google Container Structure Test validates image contents against specified criteria. Define tests for file existence, metadata, commands, and environment variables.
# structure-test.yaml
schemaVersion: '2.0.0'
fileExistenceTests:
- name: 'Gemfile.lock exists'
path: '/app/Gemfile.lock'
shouldExist: true
metadataTest:
exposedPorts: ['3000']
env:
- key: 'RAILS_ENV'
value: 'production'
commandTests:
- name: 'Ruby version'
command: 'ruby'
args: ['--version']
expectedOutput: ['ruby 3.2']
# Run structure tests
container-structure-test test \
--image myapp:v1 \
--config structure-test.yaml
Common Pitfalls
Layer Cache Invalidation
Changing early Dockerfile instructions invalidates all subsequent layer caches. A common mistake places COPY . . before dependency installation, causing dependency reinstallation on every code change.
# BAD - code changes invalidate dependency installation
FROM ruby:3.2-slim
COPY . .
RUN bundle install
# GOOD - dependency installation cached separately
FROM ruby:3.2-slim
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
Excessive Image Size
Images grow large when including unnecessary files or leaving build artifacts. Each layer adds to total image size even if later layers delete files. Deletion in subsequent layers creates a whiteout marker but preserves the data in earlier layers.
# BAD - apt cache remains in layer
FROM ruby:3.2-slim
RUN apt-get update
RUN apt-get install -y libpq-dev
RUN rm -rf /var/lib/apt/lists/* # Deletion in separate layer
# GOOD - cache removed in same layer
FROM ruby:3.2-slim
RUN apt-get update && \
apt-get install -y libpq-dev && \
rm -rf /var/lib/apt/lists/*
Use .dockerignore to exclude unnecessary files from build context:
# .dockerignore
.git
.github
tmp/
log/
coverage/
node_modules/
*.md
.env
Missing Health Checks
Images without health check definitions require external monitoring configuration. Define health checks in the Dockerfile to enable automatic container health monitoring.
FROM ruby:3.2-slim
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["bundle", "exec", "puma"]
Ignoring Build Context Size
Large build contexts slow builds and increase memory usage. The build process sends the entire context directory to the build daemon before building starts. A 2GB context directory takes significant time to transfer.
Monitor context size:
# Show context size before build
du -sh .
# Check what Docker sends
docker build --no-cache --progress=plain .
Platform Architecture Mismatches
Building images on ARM-based Macs produces ARM64 images that fail on AMD64 servers. Always specify target platforms explicitly for production images.
# Build for specific platform
docker build --platform linux/amd64 -t myapp:v1 .
# Build for multiple platforms
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:v1 \
--push \
.
Hardcoded Configuration
Embedding environment-specific configuration in images reduces portability. The same image should run in development, staging, and production with different configurations.
# BAD - hardcoded production database
FROM ruby:3.2-slim
ENV DATABASE_URL=postgresql://prod-db:5432/myapp
# GOOD - configuration from environment
FROM ruby:3.2-slim
# DATABASE_URL provided at container runtime
Stale Base Images
Using old base image versions exposes applications to known vulnerabilities. Regularly rebuild images with updated base images to receive security patches.
# Specify exact version, update regularly
FROM ruby:3.2.2-slim
# Or use digest for reproducibility
FROM ruby@sha256:abc123...
Set up automated rebuilds weekly or monthly to incorporate base image updates.
Reference
Common Dockerfile Instructions
| Instruction | Purpose | Example |
|---|---|---|
| FROM | Set base image | FROM ruby:3.2-slim |
| WORKDIR | Set working directory | WORKDIR /app |
| COPY | Copy files from build context | COPY . /app |
| ADD | Copy files with auto-extraction | ADD archive.tar.gz /app |
| RUN | Execute command in new layer | RUN bundle install |
| CMD | Default container command | CMD ["bundle", "exec", "puma"] |
| ENTRYPOINT | Container executable | ENTRYPOINT ["ruby"] |
| ENV | Set environment variable | ENV RAILS_ENV=production |
| EXPOSE | Document exposed ports | EXPOSE 3000 |
| VOLUME | Define mount point | VOLUME /app/storage |
| USER | Set user for subsequent instructions | USER appuser |
| ARG | Build-time variable | ARG RUBY_VERSION=3.2 |
| LABEL | Add metadata | LABEL version="1.0" |
| HEALTHCHECK | Container health check | HEALTHCHECK CMD curl -f http://localhost/health |
Docker CLI Commands
| Command | Purpose | Example |
|---|---|---|
| docker build | Build image from Dockerfile | docker build -t myapp:v1 . |
| docker images | List images | docker images |
| docker tag | Tag image | docker tag myapp:v1 myapp:latest |
| docker push | Upload image to registry | docker push myapp:v1 |
| docker pull | Download image from registry | docker pull ruby:3.2-slim |
| docker rmi | Remove image | docker rmi myapp:old |
| docker save | Export image to tar | docker save myapp:v1 -o image.tar |
| docker load | Import image from tar | docker load -i image.tar |
| docker history | Show image layer history | docker history myapp:v1 |
| docker inspect | Display detailed image info | docker inspect myapp:v1 |
| docker image prune | Remove unused images | docker image prune -a |
Base Image Selection Matrix
| Image Type | Size | Use Case | Trade-offs |
|---|---|---|---|
| ruby:3.2 | ~900MB | Development, CI builds | Includes build tools, git, documentation |
| ruby:3.2-slim | ~150MB | Production runtime | Minimal packages, requires explicit dependencies |
| ruby:3.2-alpine | ~50MB | Size-constrained environments | Smallest size, musl libc compatibility issues |
| Custom minimal | ~100MB | Optimized production | Full control, higher maintenance |
Multi-Stage Build Pattern
| Stage | Purpose | Contains |
|---|---|---|
| Builder | Compile and build | Build tools, compilers, development headers, source code |
| Test | Run test suite | Test frameworks, test databases, browsers for integration tests |
| Runtime | Execute application | Runtime dependencies only, compiled artifacts, application code |
Layer Optimization Strategies
| Strategy | Benefit | Implementation |
|---|---|---|
| Dependency caching | Faster rebuilds | Copy dependency files before application code |
| Command chaining | Smaller images | Combine related commands with && |
| Clean in same layer | Reduce size | Remove caches and temp files in same RUN instruction |
| .dockerignore | Faster builds | Exclude unnecessary files from build context |
| Multi-stage builds | Minimal runtime images | Separate build and runtime stages |
| Specific base tags | Reproducibility | Use version tags or digests instead of latest |
Security Best Practices Checklist
| Practice | Implementation | Verification |
|---|---|---|
| Scan for vulnerabilities | Integrate trivy/grype in CI | trivy image myapp:v1 |
| Use minimal base images | Prefer Alpine or slim variants | docker history myapp:v1 |
| Run as non-root | Create dedicated user | docker inspect myapp:v1 |
| Pin base image versions | Use digests or specific versions | Check FROM instruction |
| Avoid embedding secrets | Use runtime injection | Inspect image layers |
| Enable content trust | Set DOCKER_CONTENT_TRUST=1 | Pull with DCT enabled |
| Regular rebuilds | Automate weekly/monthly | CI/CD schedule trigger |
| Remove build artifacts | Clean in same layer | Check layer sizes |
Registry Configuration
| Registry | Authentication | Image Format |
|---|---|---|
| Docker Hub | Username/password or token | docker.io/username/image:tag |
| GitHub Container Registry | Personal access token | ghcr.io/username/image:tag |
| AWS ECR | AWS credentials | ACCOUNT.dkr.ecr.REGION.amazonaws.com/image:tag |
| Google Container Registry | Service account key | gcr.io/PROJECT/image:tag |
| Azure Container Registry | Service principal | REGISTRY.azurecr.io/image:tag |
| Self-hosted Harbor | Username/password or robot account | harbor.example.com/project/image:tag |