# Multi-stage Dockerfile for Attune sensor service # # Simple and robust: build the entire workspace, then copy the sensor binary # into different runtime base images depending on language support needed. # # Targets: # sensor-base - Native sensors only (lightweight) # sensor-full - Native + Python + Node.js sensors # # Usage: # DOCKER_BUILDKIT=1 docker build --target sensor-base -t attune-sensor:base -f docker/Dockerfile.sensor.optimized . # DOCKER_BUILDKIT=1 docker build --target sensor-full -t attune-sensor:full -f docker/Dockerfile.sensor.optimized . # # Note: Packs are NOT copied into the image — they are mounted as volumes at runtime. ARG RUST_VERSION=1.92 ARG DEBIAN_VERSION=bookworm ARG NODE_VERSION=20 # ============================================================================ # 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=33554432 # 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 && \ 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) 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, SQLx metadata, and migrations COPY .sqlx/ ./.sqlx/ 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. 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 && \ cp /build/target/release/attune-sensor /build/attune-sensor # Verify the binary was built RUN ls -lh /build/attune-sensor && \ file /build/attune-sensor # ============================================================================ # Stage 2a: Base Sensor (Native sensors only) # Runtime capabilities: native binary sensors # ============================================================================ FROM debian:${DEBIAN_VERSION}-slim AS sensor-base RUN apt-get update && apt-get install -y \ ca-certificates \ libssl3 \ curl \ bash \ procps \ && rm -rf /var/lib/apt/lists/* RUN useradd -m -u 1000 attune && \ mkdir -p /opt/attune/packs /opt/attune/logs /opt/attune/runtime_envs && \ chown -R attune:attune /opt/attune WORKDIR /opt/attune COPY --from=builder /build/attune-sensor /usr/local/bin/attune-sensor COPY config.docker.yaml ./config.yaml COPY migrations/ ./migrations/ USER attune ENV RUST_LOG=info ENV ATTUNE_CONFIG=/opt/attune/config.yaml HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \ CMD kill -0 1 || exit 1 CMD ["/usr/local/bin/attune-sensor"] # ============================================================================ # Stage 2b: Full Sensor (Native + Python + Node.js sensors) # Runtime capabilities: native, python, node # # Uses debian-slim + apt python3 + NodeSource node so that interpreter # paths (/usr/bin/python3, /usr/bin/node) are identical to the worker # containers. This avoids broken symlinks and path mismatches when # sensors and workers share the runtime_envs volume. # ============================================================================ FROM debian:${DEBIAN_VERSION}-slim AS sensor-full # Re-declare global ARG so it's available in RUN commands within this stage # (global ARGs are only automatically available in FROM instructions) ARG NODE_VERSION=20 RUN apt-get update && apt-get install -y \ ca-certificates \ libssl3 \ curl \ bash \ build-essential \ python3 \ python3-pip \ python3-venv \ procps \ && rm -rf /var/lib/apt/lists/* # Install Node.js from NodeSource (same method and version as workers) RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \ apt-get install -y nodejs && \ rm -rf /var/lib/apt/lists/* # Create python symlink for convenience RUN ln -sf /usr/bin/python3 /usr/bin/python # Install common Python packages used by sensor scripts # Use --break-system-packages for Debian 12+ pip-in-system-python restrictions RUN pip3 install --no-cache-dir --break-system-packages \ requests>=2.31.0 \ pyyaml>=6.0 \ jinja2>=3.1.0 \ python-dateutil>=2.8.0 RUN useradd -m -u 1000 attune && \ mkdir -p /opt/attune/packs /opt/attune/logs /opt/attune/runtime_envs && \ chown -R attune:attune /opt/attune WORKDIR /opt/attune COPY --from=builder /build/attune-sensor /usr/local/bin/attune-sensor COPY config.docker.yaml ./config.yaml COPY migrations/ ./migrations/ USER attune ENV RUST_LOG=info ENV ATTUNE_CONFIG=/opt/attune/config.yaml HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \ CMD kill -0 1 || exit 1 CMD ["/usr/local/bin/attune-sensor"]