CrackedRuby CrackedRuby

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