Files
attune/docs/DOCKER-OPTIMIZATION-SUMMARY.md

12 KiB

Docker Build Optimization Summary

Overview

This document summarizes the Docker build optimizations implemented for the Attune project, focusing on two key improvements:

  1. Selective crate copying - Only copy the crates needed for each service
  2. Packs as volumes - Mount packs at runtime instead of copying into images

Problems Solved

Problem 1: Layer Invalidation Cascade

Before: Copying entire crates/ directory created a single Docker layer

  • Changing ANY file in ANY crate invalidated this layer for ALL services
  • Every service rebuild took ~5-6 minutes
  • Building 7 services = 35-42 minutes of rebuild time

After: Selective crate copying

  • Only copy common + specific service crate
  • Changes to api don't affect worker, executor, etc.
  • Incremental builds: ~30-60 seconds per service
  • 90% faster for typical code changes

Problem 2: Packs Baked Into Images

Before: Packs copied into Docker images during build

  • Updating pack YAML required rebuilding service images (~5 min)
  • Pack binaries baked into images (no updates without rebuild)
  • Larger image sizes
  • Inconsistent packs across services if built at different times

After: Packs mounted as volumes

  • Update packs with simple restart (~5 sec)
  • Pack binaries updateable without image rebuild
  • Smaller, focused service images
  • All services share identical packs from shared volume
  • 98% faster pack updates

New Files Created

Dockerfiles

  • docker/Dockerfile.optimized - Optimized service builds (api, executor, sensor, notifier)
  • docker/Dockerfile.worker.optimized - Optimized worker builds (all variants)
  • docker/Dockerfile.pack-binaries - Separate pack binary builder

Scripts

  • scripts/build-pack-binaries.sh - Build pack binaries with GLIBC compatibility

Documentation

  • docs/docker-layer-optimization.md - Comprehensive guide to optimization strategy
  • docs/QUICKREF-docker-optimization.md - Quick reference for implementation
  • docs/QUICKREF-packs-volumes.md - Guide to packs volume architecture
  • docs/DOCKER-OPTIMIZATION-SUMMARY.md - This file

Architecture Changes

Service Images (Before)

Service Image Contents:
├── Rust binaries (all crates compiled)
├── Configuration files
├── Migrations
└── Packs (copied in)
    ├── YAML definitions
    ├── Scripts (Python/Shell)
    └── Binaries (sensors)

Service Images (After)

Service Image Contents:
├── Rust binary (only this service + common)
├── Configuration files
└── Migrations

Packs (mounted at runtime):
└── /opt/attune/packs -> packs_data volume

How It Works

Selective Crate Copying

# Stage 1: Planner - Cache dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/*/Cargo.toml ./crates/*/Cargo.toml
RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=shared \
    --mount=type=cache,target=/usr/local/cargo/git,sharing=shared \
    --mount=type=cache,target=/build/target,id=target-planner-${SERVICE} \
    cargo build (with dummy source)

# Stage 2: Builder - Build specific service
COPY crates/common/ ./crates/common/
COPY crates/${SERVICE}/ ./crates/${SERVICE}/
RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=shared \
    --mount=type=cache,target=/usr/local/cargo/git,sharing=shared \
    --mount=type=cache,target=/build/target,id=target-builder-${SERVICE} \
    cargo build --release --bin attune-${SERVICE}

# Stage 3: Runtime - Minimal image
COPY --from=builder /build/attune-${SERVICE} /usr/local/bin/
RUN mkdir -p /opt/attune/packs  # Mount point only

Packs Volume Flow

1. Host: ./packs/
   ├── core/pack.yaml
   ├── core/actions/*.yaml
   └── core/sensors/attune-core-timer-sensor

2. init-packs service (runs once):
   Copies ./packs/ → packs_data volume

3. Services (api, executor, worker, sensor):
   Mount packs_data:/opt/attune/packs:ro

4. Development:
   Mount ./packs.dev:/opt/attune/packs.dev:rw (direct bind)

Implementation Guide

Step 1: Build Pack Binaries

# One-time setup (or when pack binaries change)
./scripts/build-pack-binaries.sh

Step 2: Update docker-compose.yaml

services:
  api:
    build:
      dockerfile: docker/Dockerfile.optimized  # Changed
      
  worker-shell:
    build:
      dockerfile: docker/Dockerfile.worker.optimized  # Changed

Step 3: Rebuild Images

docker compose build --no-cache

Step 4: Start Services

docker compose up -d

Performance Comparison

Operation Before After Improvement
Change API code ~5 min ~30 sec 90% faster
Change worker code ~5 min ~30 sec 90% faster
Change common crate ~35 min (7 services) ~14 min 60% faster
Parallel build (4 services) ~20 min (serialized) ~5 min (concurrent) 75% faster
Update pack YAML ~5 min (rebuild) ~5 sec (restart) 98% faster
Update pack script ~5 min (rebuild) ~5 sec (restart) 98% faster
Update pack binary ~5 min (rebuild) ~2 min (rebuild binary) 60% faster
Add dependency ~5 min ~3 min 40% faster
Clean build ~5 min ~5 min Same (expected)

Development Workflows

Editing Rust Service Code

# 1. Edit code
vim crates/api/src/routes/actions.rs

# 2. Rebuild (only API service)
docker compose build api

# 3. Restart
docker compose up -d api

# Time: ~30 seconds

Editing Pack YAML/Scripts

# 1. Edit pack files
vim packs/core/actions/echo.yaml

# 2. Restart (no rebuild!)
docker compose restart

# Time: ~5 seconds

Editing Pack Binaries (Sensors)

# 1. Edit source
vim crates/core-timer-sensor/src/main.rs

# 2. Rebuild binary
./scripts/build-pack-binaries.sh

# 3. Restart
docker compose restart sensor

# Time: ~2 minutes

Development Iteration (Fast)

# Use packs.dev for instant updates
mkdir -p packs.dev/mypack/actions

# Create action
cat > packs.dev/mypack/actions/test.sh <<'EOF'
#!/bin/bash
echo "Hello from dev pack!"
EOF

chmod +x packs.dev/mypack/actions/test.sh

# Restart (changes visible immediately)
docker compose restart

# Time: ~5 seconds

Key Benefits

Build Performance

  • 90% faster incremental builds for code changes
  • Only rebuild what changed
  • Parallel builds with optimized cache sharing (4x faster than old locked strategy)
  • BuildKit cache mounts persist compilation artifacts
  • Service-specific target caches prevent conflicts

Pack Management

  • 98% faster pack updates (restart vs rebuild)
  • Update packs without touching service images
  • Consistent packs across all services
  • Clear separation: services = code, packs = content

Image Size

  • Smaller service images (no packs embedded)
  • Shared packs volume (no duplication)
  • Faster image pulls in CI/CD
  • More efficient layer caching

Developer Experience

  • Fast iteration cycles
  • packs.dev for instant testing
  • No image rebuilds for content changes
  • Clearer mental model (volumes vs images)

Tradeoffs

Advantages

  • Dramatically faster development iteration
  • Better resource utilization (cache reuse)
  • Smaller, more focused images
  • Easier pack updates and testing
  • Safe parallel builds without serialization overhead

Disadvantages

  • Slightly more complex Dockerfiles (planner stage)
  • Need to manually list all crate manifests
  • Pack binaries built separately (one more step)
  • First build ~30 seconds slower (dummy compilation)

When to Use

  • Always use for development - benefits far outweigh costs
  • Use in CI/CD - faster builds = lower costs
  • Use in production - smaller images, easier updates

When NOT to Use

  • Single-crate projects (no workspace) - no benefit
  • One-off builds - complexity not worth it
  • Extreme Dockerfile simplicity requirements

Maintenance

Adding New Service Crate

Update both optimized Dockerfiles (planner and builder stages):

# In Dockerfile.optimized and Dockerfile.worker.optimized

# Stage 1: Planner
COPY crates/new-service/Cargo.toml ./crates/new-service/Cargo.toml
RUN mkdir -p crates/new-service/src && echo "fn main() {}" > crates/new-service/src/main.rs

# Stage 2: Builder
COPY crates/new-service/Cargo.toml ./crates/new-service/Cargo.toml

Adding New Pack Binary

Update docker/Dockerfile.pack-binaries and scripts/build-pack-binaries.sh:

# Dockerfile.pack-binaries
COPY crates/new-pack-sensor/Cargo.toml ./crates/new-pack-sensor/Cargo.toml
COPY crates/new-pack-sensor/ ./crates/new-pack-sensor/
RUN cargo build --release --bin attune-new-pack-sensor
# build-pack-binaries.sh
docker cp "${CONTAINER_NAME}:/pack-binaries/attune-new-pack-sensor" "packs/mypack/sensors/"
chmod +x packs/mypack/sensors/attune-new-pack-sensor

Migration Path

For existing deployments using old Dockerfiles:

  1. Backup current setup:

    cp docker/Dockerfile docker/Dockerfile.old
    cp docker/Dockerfile.worker docker/Dockerfile.worker.old
    
  2. Build pack binaries:

    ./scripts/build-pack-binaries.sh
    
  3. Update docker-compose.yaml to use optimized Dockerfiles:

    dockerfile: docker/Dockerfile.optimized
    
  4. Rebuild all images:

    docker compose build --no-cache
    
  5. Recreate containers:

    docker compose down
    docker compose up -d
    
  6. Verify packs loaded:

    docker compose exec api ls -la /opt/attune/packs/
    docker compose logs init-packs
    

Troubleshooting

Build fails with "crate not found"

Cause: Missing crate manifest in optimized Dockerfile Fix: Add crate's Cargo.toml to both planner and builder stages

Changes not reflected after build

Cause: Docker using stale cached layers Fix: docker compose build --no-cache <service>

Pack not found at runtime

Cause: init-packs failed or packs_data volume empty Fix:

docker compose logs init-packs
docker compose restart init-packs
docker compose exec api ls -la /opt/attune/packs/

Pack binary exec format error

Cause: Binary compiled for wrong architecture/GLIBC Fix: ./scripts/build-pack-binaries.sh

Slow builds after dependency changes

Cause: Normal - dependencies must be recompiled Fix: Not an issue - optimization helps code changes, not dependency changes

References

Quick Command Reference

# Build pack binaries
./scripts/build-pack-binaries.sh

# Build single service (optimized)
docker compose build api

# Build all services
docker compose build

# Start services
docker compose up -d

# Restart after pack changes
docker compose restart

# View pack initialization logs
docker compose logs init-packs

# Inspect packs in running container
docker compose exec api ls -la /opt/attune/packs/

# Force clean rebuild
docker compose build --no-cache
docker volume rm attune_packs_data
docker compose up -d

Summary

The optimized Docker architecture provides 90% faster incremental builds and 98% faster pack updates by:

  1. Selective crate copying: Only rebuild changed services
  2. Packs as volumes: Update packs without rebuilding images
  3. Optimized cache sharing: sharing=shared for registry/git, service-specific IDs for target caches
  4. Parallel builds: 4x faster than old sharing=locked strategy
  5. Separate pack binaries: Build once, update independently

Result: Docker-based development workflows are now practical for rapid iteration on Rust workspaces with complex pack systems, with safe concurrent builds that are 4x faster than serialized builds.