# Multi-stage Dockerfile for Attune Rust services (api, executor, sensor, notifier) # # Simple and robust: build the entire workspace, then copy the target binary. # No dummy sources, no selective crate copying, no fragile hacks. # # Usage: # DOCKER_BUILDKIT=1 docker build --build-arg SERVICE=api -f docker/Dockerfile.optimized -t attune-api . # DOCKER_BUILDKIT=1 docker build --build-arg SERVICE=executor -f docker/Dockerfile.optimized -t attune-executor . # DOCKER_BUILDKIT=1 docker build --build-arg SERVICE=sensor -f docker/Dockerfile.optimized -t attune-sensor . # DOCKER_BUILDKIT=1 docker build --build-arg SERVICE=notifier -f docker/Dockerfile.optimized -t attune-notifier . # # Note: Packs are NOT copied into the image — they are mounted as volumes at runtime. ARG RUST_VERSION=1.92 ARG DEBIAN_VERSION=bookworm # ============================================================================ # Stage 1: Builder - Compile the entire workspace # ============================================================================ FROM rust:${RUST_VERSION}-${DEBIAN_VERSION} AS builder RUN apt-get update && apt-get install -y \ pkg-config \ libssl-dev \ ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /build # Increase rustc stack size to prevent SIGSEGV during release builds ENV RUST_MIN_STACK=67108864 # Copy dependency metadata first so `cargo fetch` layer is cached # when only source code changes (Cargo.toml/Cargo.lock stay the same) COPY Cargo.toml Cargo.lock ./ 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 minimal stub sources so cargo can resolve the workspace and fetch deps. # These are ONLY used for `cargo fetch` — never compiled. RUN mkdir -p crates/common/src && echo "" > crates/common/src/lib.rs && \ mkdir -p crates/api/src && echo "fn main(){}" > crates/api/src/main.rs && \ mkdir -p crates/executor/src && echo "fn main(){}" > crates/executor/src/main.rs && \ mkdir -p crates/executor/benches && echo "fn main(){}" > crates/executor/benches/context_clone.rs && \ mkdir -p crates/sensor/src && echo "fn main(){}" > crates/sensor/src/main.rs && \ mkdir -p crates/core-timer-sensor/src && echo "fn main(){}" > crates/core-timer-sensor/src/main.rs && \ mkdir -p crates/worker/src && echo "fn main(){}" > crates/worker/src/main.rs && \ echo "fn main(){}" > crates/worker/src/agent_main.rs && \ mkdir -p crates/notifier/src && echo "fn main(){}" > crates/notifier/src/main.rs && \ mkdir -p crates/cli/src && echo "fn main(){}" > crates/cli/src/main.rs # Download all dependencies (cached unless Cargo.toml/Cargo.lock change) # registry/git use sharing=shared — cargo handles concurrent reads safely RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=shared \ --mount=type=cache,target=/usr/local/cargo/git,sharing=shared \ cargo fetch # Now copy the real source code and migrations COPY migrations/ ./migrations/ COPY crates/ ./crates/ # Build the entire workspace in release mode. # All binaries are compiled together, sharing dependency compilation. # target cache uses sharing=locked so concurrent service builds serialize # writes to the shared compilation cache instead of corrupting it. # # IMPORTANT: ARG SERVICE is declared AFTER this RUN so that changing the # SERVICE value does not invalidate the cached build layer. The first # service to build compiles the full workspace; subsequent services get # a cache hit here and skip straight to the cp below. 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 --workspace --bins -j 4 # Extract the requested service binary from the target cache. # This is the only layer that varies per SERVICE value. ARG SERVICE=api RUN --mount=type=cache,target=/build/target,sharing=locked \ cp /build/target/release/attune-${SERVICE} /build/attune-service-binary # ============================================================================ # Stage 2: Runtime - Minimal image with just the service binary # ============================================================================ FROM debian:${DEBIAN_VERSION}-slim AS runtime RUN apt-get update && apt-get install -y \ ca-certificates \ libssl3 \ curl \ git \ && rm -rf /var/lib/apt/lists/* # Create non-root user and directories # /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 /opt/attune/runtime_envs /opt/attune/config && \ chown -R attune:attune /opt/attune WORKDIR /opt/attune # Copy the service binary from builder using a fixed path (no variable in COPY source) # This avoids the circular dependency Docker hits when using ARG in --from paths COPY --from=builder /build/attune-service-binary /usr/local/bin/attune-service # Copy migrations only. Runtime config is mounted via Docker Compose. COPY migrations/ ./migrations/ RUN chown -R attune:attune /opt/attune USER attune ENV RUST_LOG=info ENV ATTUNE_CONFIG=/opt/attune/config/config.yaml HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 EXPOSE 8080 CMD ["/usr/local/bin/attune-service"]