# Optimized Multi-stage Dockerfile for Attune Rust services # This Dockerfile minimizes layer invalidation by selectively copying only required crates # # Key optimizations: # 1. Copy only Cargo.toml files first to cache dependency downloads # 2. Build dummy binaries to cache compiled dependencies # 3. Copy only the specific crate being built (plus common) # 4. Use BuildKit cache mounts for cargo registry and build artifacts # # Usage: DOCKER_BUILDKIT=1 docker build --build-arg SERVICE=api -f docker/Dockerfile.optimized -t attune-api . # # Build time comparison (after common crate changes): # - Old: ~5 minutes (rebuilds all dependencies) # - New: ~30 seconds (only recompiles changed code) # # Note: This Dockerfile does NOT copy packs into the image. # Packs are mounted as volumes at runtime from the packs_data volume. # The init-packs service in docker-compose.yaml handles pack initialization. ARG RUST_VERSION=1.92 ARG DEBIAN_VERSION=bookworm # ============================================================================ # Stage 1: Planner - Extract dependency information # ============================================================================ FROM rust:${RUST_VERSION}-${DEBIAN_VERSION} AS planner # Install build dependencies RUN apt-get update && apt-get install -y \ pkg-config \ libssl-dev \ ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /build # Copy only Cargo.toml and Cargo.lock to understand dependencies COPY Cargo.toml Cargo.lock ./ # Copy all crate manifests (but not source code) # This allows cargo to resolve the workspace without needing source COPY crates/common/Cargo.toml ./crates/common/Cargo.toml COPY crates/api/Cargo.toml ./crates/api/Cargo.toml COPY crates/executor/Cargo.toml ./crates/executor/Cargo.toml COPY crates/sensor/Cargo.toml ./crates/sensor/Cargo.toml COPY crates/core-timer-sensor/Cargo.toml ./crates/core-timer-sensor/Cargo.toml COPY crates/worker/Cargo.toml ./crates/worker/Cargo.toml COPY crates/notifier/Cargo.toml ./crates/notifier/Cargo.toml COPY crates/cli/Cargo.toml ./crates/cli/Cargo.toml # Create dummy lib.rs and main.rs files for all crates # This allows us to build dependencies without the actual source code RUN mkdir -p crates/common/src && echo "fn main() {}" > crates/common/src/lib.rs RUN mkdir -p crates/api/src && echo "fn main() {}" > crates/api/src/main.rs RUN mkdir -p crates/executor/src && echo "fn main() {}" > crates/executor/src/main.rs RUN mkdir -p crates/executor/benches && echo "fn main() {}" > crates/executor/benches/context_clone.rs RUN mkdir -p crates/sensor/src && echo "fn main() {}" > crates/sensor/src/main.rs RUN mkdir -p crates/core-timer-sensor/src && echo "fn main() {}" > crates/core-timer-sensor/src/main.rs RUN mkdir -p crates/worker/src && echo "fn main() {}" > crates/worker/src/main.rs RUN mkdir -p crates/notifier/src && echo "fn main() {}" > crates/notifier/src/main.rs RUN mkdir -p crates/cli/src && echo "fn main() {}" > crates/cli/src/main.rs # Copy SQLx metadata for compile-time query checking COPY .sqlx/ ./.sqlx/ # Build argument to specify which service to build ARG SERVICE=api # Build dependencies only (with dummy source) # This layer is only invalidated when Cargo.toml or Cargo.lock changes # BuildKit cache mounts persist cargo registry and git cache # - registry/git use sharing=shared (cargo handles concurrent access safely) # - target uses service-specific cache ID to avoid conflicts between services 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 --release --bin attune-${SERVICE} || true # ============================================================================ # Stage 2: Builder - Compile the actual service # ============================================================================ FROM rust:${RUST_VERSION}-${DEBIAN_VERSION} AS builder # Install build dependencies RUN apt-get update && apt-get install -y \ pkg-config \ libssl-dev \ ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /build # Copy workspace configuration COPY Cargo.toml Cargo.lock ./ # Copy all crate manifests (required for workspace resolution) COPY crates/common/Cargo.toml ./crates/common/Cargo.toml COPY crates/api/Cargo.toml ./crates/api/Cargo.toml COPY crates/executor/Cargo.toml ./crates/executor/Cargo.toml COPY crates/sensor/Cargo.toml ./crates/sensor/Cargo.toml COPY crates/core-timer-sensor/Cargo.toml ./crates/core-timer-sensor/Cargo.toml COPY crates/worker/Cargo.toml ./crates/worker/Cargo.toml COPY crates/notifier/Cargo.toml ./crates/notifier/Cargo.toml COPY crates/cli/Cargo.toml ./crates/cli/Cargo.toml # Create dummy source files for workspace members that won't be built # This satisfies workspace resolution without copying full source RUN mkdir -p crates/api/src && echo "fn main() {}" > crates/api/src/main.rs RUN mkdir -p crates/executor/src && echo "fn main() {}" > crates/executor/src/main.rs RUN mkdir -p crates/executor/benches && echo "fn main() {}" > crates/executor/benches/context_clone.rs RUN mkdir -p crates/sensor/src && echo "fn main() {}" > crates/sensor/src/main.rs RUN mkdir -p crates/core-timer-sensor/src && echo "fn main() {}" > crates/core-timer-sensor/src/main.rs RUN mkdir -p crates/worker/src && echo "fn main() {}" > crates/worker/src/main.rs RUN mkdir -p crates/notifier/src && echo "fn main() {}" > crates/notifier/src/main.rs RUN mkdir -p crates/cli/src && echo "fn main() {}" > crates/cli/src/main.rs # Copy SQLx metadata COPY .sqlx/ ./.sqlx/ # Copy migrations (required for some services) COPY migrations/ ./migrations/ # Copy the common crate (almost all services depend on this) COPY crates/common/ ./crates/common/ # Build the specified service # The cargo registry and git cache are pre-populated from the planner stage # Only the actual compilation happens here # - registry/git use sharing=shared (concurrent builds of different services are safe) # - target uses service-specific cache ID (each service compiles different crates) 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,sharing=locked \ cargo build --release --lib -p attune-common # Build argument to specify which service to build ARG SERVICE=api # Copy only the source for the service being built # This is the key optimization: changes to other crates won't invalidate this layer COPY crates/${SERVICE}/ ./crates/${SERVICE}/ # Build the specified service # The cargo registry and git cache are pre-populated from the planner stage # Only the actual compilation happens here # - registry/git use sharing=shared (concurrent builds of different services are safe) # - target uses service-specific cache ID (each service compiles different crates) 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,sharing=shared \ cargo build --release --bin attune-${SERVICE} && \ cp /build/target/release/attune-${SERVICE} /build/attune-service-binary # ============================================================================ # Stage 3: Runtime - Create minimal runtime image # ============================================================================ FROM debian:${DEBIAN_VERSION}-slim AS runtime # Install runtime dependencies RUN apt-get update && apt-get install -y \ ca-certificates \ libssl3 \ curl \ && rm -rf /var/lib/apt/lists/* # Create non-root user and directories # Note: /opt/attune/packs is mounted as a volume at runtime, not copied in RUN useradd -m -u 1000 attune && \ mkdir -p /opt/attune/packs /opt/attune/logs && \ chown -R attune:attune /opt/attune WORKDIR /opt/attune # Copy the service binary from builder COPY --from=builder /build/attune-service-binary /usr/local/bin/attune-service # Copy configuration file for Docker Compose development # In production, mount config files as a volume instead of baking them into the image COPY config.docker.yaml ./config.yaml # Copy migrations for services that need them COPY migrations/ ./migrations/ # Note: Packs are NOT copied into the image # They are mounted as a volume at runtime from the packs_data volume # The init-packs service populates the packs_data volume from ./packs directory # Pack binaries (like attune-core-timer-sensor) are also in the mounted volume # Set ownership (packs will be mounted at runtime) RUN chown -R attune:attune /opt/attune # Switch to non-root user USER attune # Environment variables (can be overridden at runtime) ENV RUST_LOG=info ENV ATTUNE_CONFIG=/opt/attune/config.yaml # Health check (will be overridden per service in docker-compose) HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 # Expose default port (override per service) EXPOSE 8080 # Run the service CMD ["/usr/local/bin/attune-service"]