Docker Guide

This guide covers containerizing Armature applications with Docker for consistent, portable deployments.

Table of Contents

Overview

Docker provides consistent deployment environments for Armature applications. Benefits include:

  • Consistent environments across development, staging, and production
  • Easy scaling with container orchestration
  • Isolation from host system
  • Reproducible builds with multi-stage Dockerfiles

Basic Dockerfile

FROM rust:1.75-slim-bookworm as builder

WORKDIR /app
COPY . .

RUN cargo build --release

FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/my-api /usr/local/bin/

EXPOSE 3000

CMD ["my-api"]

Multi-Stage Build

Optimized Dockerfile with caching for faster builds:

# Stage 1: Build dependencies
FROM rust:1.75-slim-bookworm as deps

WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*

# Create a dummy project to cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release && rm -rf src

# Stage 2: Build application
FROM deps as builder

COPY src ./src
RUN touch src/main.rs && cargo build --release

# Stage 3: Runtime
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y \
    ca-certificates \
    libssl3 \
    && rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN useradd -r -s /bin/false appuser

WORKDIR /app
COPY --from=builder /app/target/release/my-api /app/

# Set ownership
RUN chown -R appuser:appuser /app

USER appuser

EXPOSE 3000

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

CMD ["./my-api"]

Docker Compose

Development Setup

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src
    environment:
      - RUST_LOG=debug
      - DATABASE_URL=postgres://user:pass@db:5432/app
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis

  db:
    image: postgres:15
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

Production Setup with Ferron

version: '3.8'

services:
  ferron:
    image: ferronweb/ferron:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./ferron.conf:/etc/ferron/ferron.conf:ro
      - certs:/var/lib/ferron/certs
    depends_on:
      - app

  app:
    build:
      context: .
      dockerfile: Dockerfile
    expose:
      - "3000"
    environment:
      - RUST_LOG=info
      - DATABASE_URL=postgres://user:pass@db:5432/app
      - REDIS_URL=redis://redis:6379
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '1'
          memory: 512M
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

  db:
    image: postgres:15
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d app"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  certs:
  pgdata:
  redisdata:

Best Practices

1. Use Multi-Stage Builds

Separate build and runtime stages for smaller images:

# Build stage
FROM rust:1.75 as builder
# ... build steps

# Runtime stage
FROM debian:bookworm-slim
# ... only runtime files

2. Run as Non-Root User

RUN useradd -r -s /bin/false appuser
USER appuser

3. Add Health Checks

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

4. Use .dockerignore

target/
.git/
.env
*.md
tests/
docs/

5. Set Resource Limits

deploy:
  resources:
    limits:
      cpus: '1'
      memory: 512M
    reservations:
      cpus: '0.25'
      memory: 256M

6. Use Slim Base Images

Prefer debian:bookworm-slim or alpine over full images.

7. Cache Dependencies

Copy Cargo.toml and Cargo.lock first, then build deps before copying source.

Common Patterns

Development Hot Reload

# Dockerfile.dev
FROM rust:1.75

RUN cargo install cargo-watch

WORKDIR /app
COPY Cargo.toml Cargo.lock ./

CMD ["cargo", "watch", "-x", "run"]

With Static Assets

# Build frontend
FROM node:20 as frontend
WORKDIR /web
COPY web/package*.json ./
RUN npm ci
COPY web/ ./
RUN npm run build

# Build backend
FROM rust:1.75 as backend
WORKDIR /app
COPY . .
RUN cargo build --release

# Runtime
FROM debian:bookworm-slim
COPY --from=backend /app/target/release/my-api /app/
COPY --from=frontend /web/dist /app/static/
CMD ["/app/my-api"]

With Ferron Sidecar

# ferron.Dockerfile
FROM ferronweb/ferron:latest
COPY ferron.conf /etc/ferron/ferron.conf
CMD ["ferron", "-c", "/etc/ferron/ferron.conf"]

Summary

  • Use multi-stage builds for smaller images
  • Run as non-root user for security
  • Add health checks for orchestration
  • Use Docker Compose for local development
  • Set resource limits in production
  • Separate development and production Dockerfiles