425 lines
12 KiB
Markdown
425 lines
12 KiB
Markdown
# 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
|
|
|
|
```dockerfile
|
|
# 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
|
|
```bash
|
|
# One-time setup (or when pack binaries change)
|
|
./scripts/build-pack-binaries.sh
|
|
```
|
|
|
|
### Step 2: Update docker-compose.yaml
|
|
```yaml
|
|
services:
|
|
api:
|
|
build:
|
|
dockerfile: docker/Dockerfile.optimized # Changed
|
|
|
|
worker-shell:
|
|
build:
|
|
dockerfile: docker/Dockerfile.worker.optimized # Changed
|
|
```
|
|
|
|
### Step 3: Rebuild Images
|
|
```bash
|
|
docker compose build --no-cache
|
|
```
|
|
|
|
### Step 4: Start Services
|
|
```bash
|
|
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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 1. Edit pack files
|
|
vim packs/core/actions/echo.yaml
|
|
|
|
# 2. Restart (no rebuild!)
|
|
docker compose restart
|
|
|
|
# Time: ~5 seconds
|
|
```
|
|
|
|
### Editing Pack Binaries (Sensors)
|
|
```bash
|
|
# 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)
|
|
```bash
|
|
# 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):
|
|
|
|
```dockerfile
|
|
# 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
|
|
# 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
|
|
```
|
|
|
|
```bash
|
|
# 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**:
|
|
```bash
|
|
cp docker/Dockerfile docker/Dockerfile.old
|
|
cp docker/Dockerfile.worker docker/Dockerfile.worker.old
|
|
```
|
|
|
|
2. **Build pack binaries**:
|
|
```bash
|
|
./scripts/build-pack-binaries.sh
|
|
```
|
|
|
|
3. **Update docker-compose.yaml** to use optimized Dockerfiles:
|
|
```yaml
|
|
dockerfile: docker/Dockerfile.optimized
|
|
```
|
|
|
|
4. **Rebuild all images**:
|
|
```bash
|
|
docker compose build --no-cache
|
|
```
|
|
|
|
5. **Recreate containers**:
|
|
```bash
|
|
docker compose down
|
|
docker compose up -d
|
|
```
|
|
|
|
6. **Verify packs loaded**:
|
|
```bash
|
|
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**:
|
|
```bash
|
|
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
|
|
|
|
- **Full Guide**: `docs/docker-layer-optimization.md`
|
|
- **Quick Start**: `docs/QUICKREF-docker-optimization.md`
|
|
- **Packs Architecture**: `docs/QUICKREF-packs-volumes.md`
|
|
- **Docker BuildKit**: https://docs.docker.com/build/cache/
|
|
- **Volume Mounts**: https://docs.docker.com/storage/volumes/
|
|
|
|
## Quick Command Reference
|
|
|
|
```bash
|
|
# 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. |