Overview
Containers package applications with their dependencies into isolated, portable units that run consistently across different computing environments. A container encapsulates an application's code, runtime, system tools, libraries, and configuration files into a single executable package. Unlike virtual machines, containers share the host operating system's kernel while maintaining process and filesystem isolation.
The containerization model emerged from Linux kernel features developed in the mid-2000s, particularly namespaces and control groups (cgroups). Docker popularized containers in 2013 by providing a developer-friendly interface to these kernel primitives. Containers address the "works on my machine" problem by ensuring applications behave identically in development, testing, and production environments.
Container technology operates at the operating system level rather than the hardware level. Each container runs as an isolated process on the host system, with its own filesystem, network interface, and process space. The container runtime enforces resource limits and isolation boundaries while the container engine manages container lifecycle operations.
A typical containerized application consists of three layers: the base operating system image (typically a minimal Linux distribution), the runtime environment (language interpreters, system libraries), and the application code with its dependencies. This layered approach enables efficient storage through layer sharing and caching.
# A containerized Ruby application structure
/app
├── Gemfile
├── Gemfile.lock
├── app.rb
└── config/
└── database.yml
Container adoption has transformed software deployment practices. Organizations deploy thousands of containers across distributed systems, replacing traditional server provisioning with container orchestration platforms. The ephemeral nature of containers—designed to be created, destroyed, and replaced—requires different operational patterns than traditional long-lived server instances.
Key Principles
Container isolation relies on Linux kernel namespaces that partition system resources. Each namespace type isolates a specific resource category: PID namespaces separate process trees, network namespaces provide independent network stacks, mount namespaces isolate filesystem mount points, UTS namespaces separate hostname and domain names, IPC namespaces isolate inter-process communication, and user namespaces map container user IDs to different host user IDs.
Control groups (cgroups) enforce resource limits and accounting. The cgroup mechanism restricts how much CPU time, memory, disk I/O, and network bandwidth a container can consume. This prevents a single container from exhausting host resources and enables resource guarantees for critical workloads.
Container Resource Hierarchy:
├── cpu.shares (relative CPU weight)
├── memory.limit_in_bytes (hard memory limit)
├── blkio.weight (I/O priority)
└── net_cls.classid (network traffic class)
Container images use a layered filesystem where each layer represents a set of filesystem changes. The Union Filesystem (UnionFS) overlays these read-only layers, presenting them as a single filesystem. When a container starts, the runtime adds a writable layer on top where all container modifications occur. This copy-on-write mechanism enables multiple containers to share base image layers while maintaining isolation.
Image layers follow a content-addressable model where each layer is identified by a cryptographic hash of its contents. This ensures integrity and enables efficient storage through deduplication. When multiple images share common base layers, the system stores those layers once and references them multiple times.
The container execution lifecycle involves several distinct phases. Image pulling downloads and verifies layers from a registry. Container creation establishes the isolated environment with configured namespaces and cgroups. Container start executes the main process within the isolated context. Container stop sends termination signals to the main process. Container removal deletes the writable layer and releases allocated resources.
Container networking provides multiple models. Bridge networking creates a virtual network interface connected to a software bridge on the host. Host networking removes network isolation, making the container use the host's network stack directly. Overlay networking connects containers across multiple hosts using tunneling protocols. Container networking also handles port mapping, DNS resolution, and service discovery.
Storage persistence requires explicit volume management since container filesystems are ephemeral. Volumes exist independently of container lifecycles and can be mounted into multiple containers. Bind mounts map host directories into containers. tmpfs mounts provide memory-backed temporary storage.
Container security boundaries depend on correct kernel isolation. While namespaces and cgroups provide substantial isolation, they share the host kernel. A kernel vulnerability potentially affects all containers on a host. Security hardening includes running containers with minimal privileges, using read-only root filesystems, dropping unnecessary kernel capabilities, and applying seccomp and AppArmor profiles.
Implementation Approaches
The daemon-based architecture uses a persistent background service that manages container operations. Docker employs this model with the Docker daemon (dockerd) handling API requests and coordinating with the container runtime. This centralized approach simplifies client tooling but creates a single point of failure and requires elevated privileges.
The daemonless architecture eliminates the persistent daemon requirement. Podman implements this model where the client directly invokes the container runtime. Each container runs as a child process of the invoking user, reducing the attack surface and enabling rootless container operation. The daemonless model trades some operational convenience for improved security and resource efficiency.
Daemon Architecture:
Client → API → Daemon → Runtime → Container
Daemonless Architecture:
Client → Runtime → Container
Container runtime selection affects performance and compatibility characteristics. High-level runtimes like containerd and CRI-O handle image management, storage, and networking while delegating low-level execution to runc or crun. Low-level runtimes implement the OCI Runtime Specification, creating and running containers according to configuration files. Specialized runtimes like gVisor and Kata Containers provide enhanced isolation through different kernel interaction models.
Image building strategies determine build performance and layer efficiency. Sequential builds execute each instruction in order, creating a new layer per instruction. Multi-stage builds use multiple FROM instructions to separate build-time dependencies from runtime requirements, significantly reducing final image size. BuildKit, Docker's next-generation builder, enables parallel layer building and advanced caching strategies.
# Multi-stage build example
FROM ruby:3.2 AS builder
WORKDIR /build
COPY Gemfile* ./
RUN bundle install --deployment --without development test
FROM ruby:3.2-slim
WORKDIR /app
COPY --from=builder /build/vendor/bundle ./vendor/bundle
COPY . .
ENV BUNDLE_PATH=/app/vendor/bundle
CMD ["ruby", "app.rb"]
Orchestration platforms manage container deployment across clusters. Kubernetes provides declarative configuration where operators specify desired state and the system reconciles actual state to match. Docker Swarm offers a simpler model integrated with Docker tooling. Nomad supports containers alongside virtual machines and standalone executables. Orchestration handles scheduling, scaling, load balancing, self-healing, and rolling updates.
Container registries store and distribute images using standardized APIs. Public registries like Docker Hub host millions of images with varying trust levels. Private registries provide organizational control over image distribution. Registry selection affects deployment speed, security scanning, and compliance requirements. Image signing and vulnerability scanning integrate at the registry level.
Ruby Implementation
Containerizing Ruby applications requires understanding gem management, dependency resolution, and runtime configuration. The Gemfile defines application dependencies while Gemfile.lock pins exact versions. Container builds must install gems in a reproducible manner that survives image rebuilding.
FROM ruby:3.2
WORKDIR /app
# Copy dependency files first for better layer caching
COPY Gemfile Gemfile.lock ./
# Install gems without documentation
RUN bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install --jobs 4
# Copy application code
COPY . .
EXPOSE 3000
CMD ["bundle", "exec", "ruby", "app.rb"]
The bundler deployment mode installs gems into vendor/bundle within the application directory rather than system-wide locations. This approach ensures gems package with the application and remain isolated from other Ruby applications on the same system. The deployment configuration freezes gem versions, preventing accidental updates that could break compatibility.
Ruby web applications require process management within containers. Single-process containers run the application server directly as PID 1. Multi-process containers need an init system to handle signal forwarding and zombie process reaping. The tini init system provides minimal overhead while solving the PID 1 problem.
FROM ruby:3.2
# Add tini for proper signal handling
RUN apt-get update && \
apt-get install -y --no-install-recommends tini && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Gemfile* ./
RUN bundle install
COPY . .
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Environment-based configuration adapts applications to different deployment contexts. The ENV instruction sets default environment variables at build time. Container runtime flags override these defaults with environment-specific values. Ruby applications access environment variables through ENV hash.
# config/database.yml
production:
adapter: postgresql
host: <%= ENV.fetch('DATABASE_HOST', 'localhost') %>
port: <%= ENV.fetch('DATABASE_PORT', '5432') %>
database: <%= ENV.fetch('DATABASE_NAME') %>
username: <%= ENV.fetch('DATABASE_USER') %>
password: <%= ENV.fetch('DATABASE_PASSWORD') %>
pool: <%= ENV.fetch('DATABASE_POOL', '5') %>
Health checks monitor container application status. Docker HEALTHCHECK instructions define commands that verify application responsiveness. Ruby applications implement health check endpoints that verify database connectivity, cache availability, and critical service dependencies.
# health_check.rb
require 'net/http'
require 'json'
def check_health
checks = {
database: check_database,
redis: check_redis,
disk_space: check_disk_space
}
healthy = checks.values.all?
status = healthy ? 200 : 503
{
status: healthy ? 'healthy' : 'unhealthy',
checks: checks
}
end
def check_database
ActiveRecord::Base.connection.execute('SELECT 1')
true
rescue => e
false
end
Asset compilation for Rails applications requires special consideration. Precompiling assets during image build reduces container startup time but embeds build-time configuration. Runtime asset compilation increases startup time but adapts to runtime configuration. The choice depends on deployment patterns and asset complexity.
FROM ruby:3.2 AS assets
WORKDIR /app
COPY Gemfile* ./
RUN bundle install
COPY . .
RUN RAILS_ENV=production bundle exec rake assets:precompile
FROM ruby:3.2-slim
WORKDIR /app
COPY --from=assets /app/public/assets ./public/assets
COPY --from=assets /app/vendor/bundle ./vendor/bundle
COPY . .
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
Gem native extensions require build tools during installation. The pattern installs build dependencies, compiles native extensions, then removes build tools to reduce image size. Multi-stage builds cleanly separate build and runtime environments.
FROM ruby:3.2 AS builder
RUN apt-get update && \
apt-get install -y build-essential libpq-dev
COPY Gemfile* ./
RUN bundle install --deployment
FROM ruby:3.2-slim
RUN apt-get update && \
apt-get install -y libpq5 && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/bundle /usr/local/bundle
Tools & Ecosystem
Docker provides the most widely adopted container platform, combining client tooling, runtime, image building, and registry integration. The Docker CLI offers commands for building images (docker build), running containers (docker run), managing networks (docker network), handling volumes (docker volume), and orchestrating multi-container applications (docker compose).
Docker Compose defines multi-container applications using YAML configuration files. The compose file specifies services, networks, volumes, and dependencies. This declarative approach simplifies local development environments and enables consistent multi-container deployments.
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://db/myapp
REDIS_URL: redis://redis:6379
depends_on:
- db
- redis
volumes:
- .:/app
- bundle:/usr/local/bundle
db:
image: postgres:15
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
bundle:
Podman offers a Docker-compatible alternative with rootless containers and daemonless architecture. The podman command provides nearly identical syntax to Docker while running containers without requiring root privileges. Podman generates Kubernetes YAML from running containers, facilitating migration to orchestration platforms.
Buildah specializes in building container images without requiring a full container runtime. The tool constructs images from Dockerfiles or through scripting, offering fine-grained control over layer creation. Buildah's scriptable interface enables custom build workflows beyond Dockerfile capabilities.
Kubernetes orchestrates containerized applications across clusters with declarative configuration. Pods group related containers that share networking and storage. Deployments manage replica sets with rolling updates and rollback capabilities. Services provide stable network endpoints and load balancing. ConfigMaps and Secrets externalize configuration from container images.
apiVersion: apps/v1
kind: Deployment
metadata:
name: ruby-app
spec:
replicas: 3
selector:
matchLabels:
app: ruby-app
template:
metadata:
labels:
app: ruby-app
spec:
containers:
- name: app
image: myregistry/ruby-app:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
Container registries distribute images through standardized protocols. Docker Hub hosts public images with automated builds and vulnerability scanning. Harbor provides enterprise registry features including replication, RBAC, and image signing. GitHub Container Registry integrates with GitHub repositories and workflows. Registry selection impacts deployment speed, security posture, and cost.
Image scanning tools identify vulnerabilities in container images. Trivy scans images for known CVEs in operating system packages and application dependencies. Clair performs static analysis of container layers. Scanning integrates into CI/CD pipelines to prevent vulnerable images from reaching production.
Security Implications
Container isolation provides security boundaries but requires proper configuration. Running containers as non-root users limits privilege escalation risks. The USER instruction in Dockerfiles switches to a non-privileged user before starting the application.
FROM ruby:3.2-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["bundle", "exec", "ruby", "app.rb"]
Capability dropping removes unnecessary kernel privileges. Linux capabilities partition root privileges into distinct units. Containers start with a default capability set that can be further restricted. The --cap-drop flag removes capabilities while --cap-add grants specific capabilities when needed.
Read-only root filesystems prevent container modification after deployment. The --read-only flag makes the container's root filesystem immutable. Applications requiring writable storage use tmpfs mounts for temporary files or volumes for persistent data.
docker run --read-only \
--tmpfs /tmp \
--tmpfs /var/run \
-v /app/uploads:/app/uploads \
myapp:latest
Seccomp profiles restrict system calls available to containers. The default Docker seccomp profile blocks approximately 44 of 300+ system calls. Custom profiles further restrict capabilities based on application requirements. System call filtering prevents exploitation of kernel vulnerabilities through unusual system call sequences.
AppArmor and SELinux provide mandatory access control. These Linux Security Modules enforce policies limiting file access, network operations, and process capabilities. Container runtimes load security profiles that restrict container actions beyond basic namespace isolation.
Image signing verifies image authenticity and integrity. Docker Content Trust signs images with private keys and verifies signatures using public keys. The signature ensures images have not been tampered with during distribution. Admission controllers in Kubernetes can enforce signature verification before deploying containers.
Secret management requires specialized handling. Environment variables expose secrets in process listings and logs. File-based secrets mount sensitive data from orchestration platforms. Ruby applications read secrets from mounted files rather than environment variables for improved security.
# Reading secrets from mounted files
class SecretManager
def self.read_secret(name)
secret_path = File.join('/run/secrets', name)
return ENV[name.upcase] unless File.exist?(secret_path)
File.read(secret_path).strip
end
end
database_password = SecretManager.read_secret('database_password')
Network policies control traffic between containers. Kubernetes NetworkPolicy resources define ingress and egress rules based on pod labels, namespaces, and IP ranges. Default-deny policies block all traffic except explicitly allowed connections, implementing zero-trust networking.
Base image selection affects security posture. Minimal base images like Alpine Linux reduce attack surface by including fewer packages. Distroless images contain only the application and runtime dependencies without shell or package managers. The trade-off involves increased build complexity versus reduced vulnerability exposure.
Real-World Applications
Production deployments require health monitoring and graceful shutdown handling. Container orchestration platforms use liveness probes to detect stuck applications and readiness probes to determine when containers can receive traffic. Ruby applications implement these endpoints to integrate with orchestration health checking.
# Sinatra health check endpoints
require 'sinatra'
get '/health/live' do
# Liveness check: is the process running?
status 200
body 'OK'
end
get '/health/ready' do
# Readiness check: can the application serve requests?
begin
ActiveRecord::Base.connection.execute('SELECT 1')
Redis.current.ping
status 200
body 'Ready'
rescue => e
status 503
body "Not ready: #{e.message}"
end
end
Signal handling ensures clean shutdowns. Container orchestration sends SIGTERM before SIGKILL when stopping containers. Ruby applications trap signals to complete in-flight requests, close database connections, and flush logs before exiting.
# Graceful shutdown handling
trap('TERM') do
puts 'Received TERM signal, shutting down gracefully...'
# Stop accepting new connections
server.stop(timeout: 30)
# Close database connections
ActiveRecord::Base.connection_pool.disconnect!
# Flush logs
STDOUT.flush
STDERR.flush
exit(0)
end
trap('INT') do
puts 'Received INT signal, shutting down...'
exit(0)
end
Log aggregation collects output from ephemeral containers. Applications write logs to STDOUT and STDERR, which container runtimes capture. Log shipping systems collect these streams and forward to centralized logging platforms. Structured logging in JSON format facilitates parsing and analysis.
require 'logger'
require 'json'
class StructuredLogger
def initialize
@logger = Logger.new(STDOUT)
@logger.formatter = proc do |severity, datetime, progname, msg|
log_entry = {
timestamp: datetime.iso8601,
severity: severity,
message: msg,
service: ENV['SERVICE_NAME'],
environment: ENV['RAILS_ENV']
}
"#{log_entry.to_json}\n"
end
end
def info(message, **context)
@logger.info({ message: message }.merge(context).to_json)
end
end
Horizontal scaling adds container replicas to handle increased load. Orchestration platforms monitor resource utilization and automatically scale deployments. Stateless application design enables scaling since any container instance can serve any request. Session storage moves to external stores like Redis or databases.
Blue-green deployments minimize downtime during updates. The pattern maintains two identical production environments where one serves traffic while the other receives updates. After validation, traffic switches to the updated environment. Container orchestration implements this pattern through service routing updates.
Rolling updates gradually replace container instances with new versions. Kubernetes Deployments control update rate through maxSurge and maxUnavailable parameters. The orchestrator creates new pods before terminating old ones, maintaining service availability throughout the update process.
Resource requests and limits prevent resource exhaustion. Requests guarantee minimum resources while limits cap maximum consumption. Proper resource allocation prevents node overcommitment and ensures application performance.
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Monitoring collects metrics from containerized applications. Prometheus scrapes metrics endpoints exposing application and runtime metrics. Ruby applications expose metrics through the prometheus_exporter gem. Metrics include request rates, error rates, latency percentiles, and resource utilization.
require 'prometheus_exporter'
require 'prometheus_exporter/server'
require 'prometheus_exporter/middleware'
use PrometheusExporter::Middleware
# Custom metric tracking
counter = PrometheusExporter::Metric::Counter.new(
'processed_orders_total',
'Total number of processed orders'
)
gauge = PrometheusExporter::Metric::Gauge.new(
'queue_depth',
'Current queue depth'
)
histogram = PrometheusExporter::Metric::Histogram.new(
'request_duration_seconds',
'Request duration in seconds'
)
Reference
Container Lifecycle Commands
| Command | Purpose | Example |
|---|---|---|
| docker build | Build image from Dockerfile | docker build -t myapp:1.0 . |
| docker run | Create and start container | docker run -d -p 3000:3000 myapp:1.0 |
| docker ps | List running containers | docker ps -a |
| docker stop | Stop running container | docker stop container_id |
| docker rm | Remove container | docker rm container_id |
| docker logs | View container output | docker logs -f container_id |
| docker exec | Execute command in container | docker exec -it container_id bash |
| docker pull | Download image | docker pull ruby:3.2 |
| docker push | Upload image to registry | docker push registry/myapp:1.0 |
Dockerfile Instructions
| Instruction | Purpose | Usage |
|---|---|---|
| FROM | Set base image | FROM ruby:3.2-slim |
| WORKDIR | Set working directory | WORKDIR /app |
| COPY | Copy files into image | COPY . /app |
| ADD | Copy and extract archives | ADD archive.tar.gz /app |
| RUN | Execute commands during build | RUN bundle install |
| ENV | Set environment variables | ENV RAILS_ENV=production |
| EXPOSE | Document port usage | EXPOSE 3000 |
| CMD | Default container command | CMD ["ruby", "app.rb"] |
| ENTRYPOINT | Container entry point | ENTRYPOINT ["/usr/bin/tini"] |
| USER | Switch user context | USER appuser |
| VOLUME | Declare volume mount point | VOLUME /app/data |
| ARG | Build-time variables | ARG RUBY_VERSION=3.2 |
Resource Limit Parameters
| Parameter | Purpose | Value Format |
|---|---|---|
| memory | Maximum memory | 512m, 1g, 2048m |
| cpu | CPU shares | 1, 0.5, 2000m |
| memory-reservation | Soft memory limit | 256m, 512m |
| cpus | CPU count limit | 1.5, 2.0 |
| pids-limit | Process limit | 100, 500 |
| blkio-weight | Block I/O weight | 10-1000 |
Common Runtime Flags
| Flag | Purpose | Example |
|---|---|---|
| -d | Detached mode | docker run -d myapp |
| -p | Port mapping | docker run -p 3000:3000 myapp |
| -e | Environment variable | docker run -e DATABASE_URL=postgres://db myapp |
| -v | Volume mount | docker run -v /host:/container myapp |
| --name | Container name | docker run --name webapp myapp |
| --network | Network selection | docker run --network mynet myapp |
| --restart | Restart policy | docker run --restart unless-stopped myapp |
| --read-only | Read-only filesystem | docker run --read-only myapp |
| --cap-drop | Drop capabilities | docker run --cap-drop ALL myapp |
| --security-opt | Security options | docker run --security-opt no-new-privileges myapp |
Image Layer Optimization
| Technique | Impact | Implementation |
|---|---|---|
| Multi-stage builds | Reduces final size 50-90% | Multiple FROM statements |
| Layer caching | Faster rebuilds | Order instructions by change frequency |
| Combine RUN commands | Fewer layers | Chain commands with && |
| .dockerignore | Excludes unnecessary files | Create .dockerignore file |
| Minimal base images | Smaller attack surface | Use alpine or distroless images |
| Remove build dependencies | Smaller runtime image | apt-get purge or multi-stage |
Network Modes
| Mode | Isolation | Use Case |
|---|---|---|
| bridge | Containers share virtual network | Default mode, container-to-container communication |
| host | No isolation, uses host network | Maximum performance, port conflicts possible |
| none | No networking | Isolated containers, manual configuration |
| overlay | Multi-host networking | Swarm and Kubernetes clusters |
| macvlan | Physical network integration | Legacy applications requiring MAC addresses |
Volume Types
| Type | Persistence | Management |
|---|---|---|
| Named volumes | Docker-managed | docker volume create |
| Bind mounts | Host directory | -v /host/path:/container/path |
| tmpfs mounts | Memory only | --tmpfs /path |
| Volume plugins | External storage | NFS, AWS EFS, Ceph |