# Optimized Multi-stage Dockerfile for Attune workers # 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 worker and common crates (not all crates) # 4. Use BuildKit cache mounts for cargo registry and build artifacts # # Supports building different worker variants with different runtime capabilities # # Usage: # docker build --target worker-base -t attune-worker:base -f docker/Dockerfile.worker.optimized . # docker build --target worker-python -t attune-worker:python -f docker/Dockerfile.worker.optimized . # docker build --target worker-node -t attune-worker:node -f docker/Dockerfile.worker.optimized . # docker build --target worker-full -t attune-worker:full -f docker/Dockerfile.worker.optimized . ARG RUST_VERSION=1.92 ARG DEBIAN_VERSION=bookworm ARG PYTHON_VERSION=3.11 ARG NODE_VERSION=20 # ============================================================================ # 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 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 to satisfy cargo 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 COPY .sqlx/ ./.sqlx/ # Build dependencies only (with dummy source) # This layer is cached and only invalidated when dependencies change # - registry/git use sharing=shared (cargo handles concurrent access safely) # - target uses private cache for planner stage 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-worker-planner \ cargo build --release --bin attune-worker || true # ============================================================================ # Stage 2: Builder - Compile the worker binary # ============================================================================ 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/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 by common crate) COPY migrations/ ./migrations/ # Copy ONLY the crates needed for worker # This is the key optimization: changes to api/executor/sensor/notifier/cli won't invalidate this layer COPY crates/common/ ./crates/common/ COPY crates/worker/ ./crates/worker/ # Build the worker binary # Dependencies are already cached from planner stage # - registry/git use sharing=shared (concurrent builds are safe) # - target uses dedicated cache for worker builds (all workers share same binary) 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-worker-builder \ cargo build --release --bin attune-worker && \ cp /build/target/release/attune-worker /build/attune-worker # Verify the binary was built RUN ls -lh /build/attune-worker && \ file /build/attune-worker && \ /build/attune-worker --version || echo "Version check skipped" # ============================================================================ # Stage 3a: Base Worker (Shell only) # Runtime capabilities: shell # Use case: Lightweight workers for shell scripts and basic automation # ============================================================================ FROM debian:${DEBIAN_VERSION}-slim AS worker-base # Install runtime dependencies RUN apt-get update && apt-get install -y \ ca-certificates \ libssl3 \ curl \ bash \ procps \ && rm -rf /var/lib/apt/lists/* # Create worker 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 worker binary from builder COPY --from=builder /build/attune-worker /usr/local/bin/attune-worker # Copy configuration template COPY config.docker.yaml ./config.yaml # 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 # Switch to non-root user USER attune # Environment variables ENV ATTUNE_WORKER_RUNTIMES="shell" ENV ATTUNE_WORKER_TYPE="container" ENV RUST_LOG=info ENV ATTUNE_CONFIG=/opt/attune/config.yaml # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD pgrep -f attune-worker || exit 1 # Run the worker CMD ["/usr/local/bin/attune-worker"] # ============================================================================ # Stage 3b: Python Worker (Shell + Python) # Runtime capabilities: shell, python # Use case: Python actions and scripts with dependencies # ============================================================================ FROM python:${PYTHON_VERSION}-slim-${DEBIAN_VERSION} AS worker-python # Install system dependencies RUN apt-get update && apt-get install -y \ ca-certificates \ libssl3 \ curl \ build-essential \ procps \ && rm -rf /var/lib/apt/lists/* # Install common Python packages # These are commonly used in automation scripts RUN pip install --no-cache-dir \ requests>=2.31.0 \ pyyaml>=6.0 \ jinja2>=3.1.0 \ python-dateutil>=2.8.0 # Create worker user and directories # Note: /opt/attune/packs is mounted as a volume at runtime, not copied in RUN useradd -m -u 1001 attune && \ mkdir -p /opt/attune/packs /opt/attune/logs && \ chown -R attune:attune /opt/attune WORKDIR /opt/attune # Copy worker binary from builder COPY --from=builder /build/attune-worker /usr/local/bin/attune-worker # Copy configuration template COPY config.docker.yaml ./config.yaml # Note: Packs are NOT copied into the image # They are mounted as a volume at runtime from the packs_data volume # Switch to non-root user USER attune # Environment variables ENV ATTUNE_WORKER_RUNTIMES="shell,python" ENV ATTUNE_WORKER_TYPE="container" ENV RUST_LOG=info ENV ATTUNE_CONFIG=/opt/attune/config.yaml # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD pgrep -f attune-worker || exit 1 # Run the worker CMD ["/usr/local/bin/attune-worker"] # ============================================================================ # Stage 3c: Node Worker (Shell + Node.js) # Runtime capabilities: shell, node # Use case: JavaScript/TypeScript actions and npm packages # ============================================================================ FROM node:${NODE_VERSION}-slim AS worker-node # Install system dependencies RUN apt-get update && apt-get install -y \ ca-certificates \ libssl3 \ curl \ procps \ && rm -rf /var/lib/apt/lists/* # Create worker user and directories # Note: Node base image has 'node' user at UID 1000, so we use UID 1001 # Note: /opt/attune/packs is mounted as a volume at runtime, not copied in RUN useradd -m -u 1001 attune && \ mkdir -p /opt/attune/packs /opt/attune/logs && \ chown -R attune:attune /opt/attune WORKDIR /opt/attune # Copy worker binary from builder COPY --from=builder /build/attune-worker /usr/local/bin/attune-worker # Copy configuration template COPY config.docker.yaml ./config.yaml # Note: Packs are NOT copied into the image # They are mounted as a volume at runtime from the packs_data volume # Switch to non-root user USER attune # Environment variables ENV ATTUNE_WORKER_RUNTIMES="shell,node" ENV ATTUNE_WORKER_TYPE="container" ENV RUST_LOG=info ENV ATTUNE_CONFIG=/opt/attune/config.yaml # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD pgrep -f attune-worker || exit 1 # Run the worker CMD ["/usr/local/bin/attune-worker"] # ============================================================================ # Stage 3d: Full Worker (All runtimes) # Runtime capabilities: shell, python, node, native # Use case: General-purpose automation with multi-language support # ============================================================================ FROM debian:${DEBIAN_VERSION} AS worker-full # Install system dependencies including Python and Node.js RUN apt-get update && apt-get install -y \ ca-certificates \ libssl3 \ curl \ build-essential \ python3 \ python3-pip \ python3-venv \ procps \ && rm -rf /var/lib/apt/lists/* # Install Node.js from NodeSource RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ apt-get install -y nodejs && \ rm -rf /var/lib/apt/lists/* # Create python symlink for convenience RUN ln -s /usr/bin/python3 /usr/bin/python # Install common Python packages # 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 # Create worker user and directories # Note: /opt/attune/packs is mounted as a volume at runtime, not copied in RUN useradd -m -u 1001 attune && \ mkdir -p /opt/attune/packs /opt/attune/logs && \ chown -R attune:attune /opt/attune WORKDIR /opt/attune # Copy worker binary from builder COPY --from=builder /build/attune-worker /usr/local/bin/attune-worker # Copy configuration template COPY config.docker.yaml ./config.yaml # Note: Packs are NOT copied into the image # They are mounted as a volume at runtime from the packs_data volume # Switch to non-root user USER attune # Environment variables ENV ATTUNE_WORKER_RUNTIMES="shell,python,node,native" ENV ATTUNE_WORKER_TYPE="container" ENV RUST_LOG=info ENV ATTUNE_CONFIG=/opt/attune/config.yaml # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD pgrep -f attune-worker || exit 1 # Run the worker CMD ["/usr/local/bin/attune-worker"]