re-uploading work

This commit is contained in:
2026-02-04 17:46:30 -06:00
commit 3b14c65998
1388 changed files with 381262 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
┌─────────────────────────────────────────────────────────────┐
│ DOCKER BUILD QUICK REFERENCE │
│ Fixing Build Race Conditions │
└─────────────────────────────────────────────────────────────┘
🚀 FASTEST & MOST RELIABLE (Recommended):
───────────────────────────────────────────────────────────────
make docker-cache-warm # ~5-6 min
make docker-build # ~15-20 min
make docker-up # Start services
📋 ALTERNATIVE (Simple but slower):
───────────────────────────────────────────────────────────────
docker compose build # ~25-30 min (sequential)
docker compose up -d
⚡ SINGLE SERVICE (Development):
───────────────────────────────────────────────────────────────
docker compose build api
docker compose up -d api
❌ ERROR: "File exists (os error 17)"
───────────────────────────────────────────────────────────────
docker builder prune -af
make docker-cache-warm
make docker-build
📚 MORE INFO:
───────────────────────────────────────────────────────────────
docker/BUILD_QUICKSTART.md
docker/DOCKER_BUILD_RACE_CONDITIONS.md

194
docker/BUILD_QUICKSTART.md Normal file
View File

@@ -0,0 +1,194 @@
# Docker Build Quick Start
## TL;DR - Fastest & Most Reliable Build
```bash
make docker-cache-warm # ~5-6 minutes (first time only)
make docker-build # ~15-20 minutes (first time), ~2-5 min (incremental)
make docker-up # Start all services
```
## Why Two Steps?
Building multiple Rust services in parallel can cause race conditions in the shared Cargo cache. Pre-warming the cache prevents this.
## Build Methods
### Method 1: Cache Warming (Recommended)
**Best for: First-time builds, after dependency updates**
```bash
# Step 1: Pre-load cache
make docker-cache-warm
# Step 2: Build all services
make docker-build
# Step 3: Start
make docker-up
```
⏱️ **Timing**: ~20-25 min first time, ~2-5 min incremental
### Method 2: Direct Build
**Best for: Quick builds, incremental changes**
```bash
docker compose build
make docker-up
```
⏱️ **Timing**: ~25-30 min first time (sequential due to cache locking), ~2-5 min incremental
### Method 3: Single Service
**Best for: Developing one service**
```bash
docker compose build api
docker compose up -d api
```
⏱️ **Timing**: ~5-6 min first time, ~30-60 sec incremental
## Common Commands
| Command | Description | Time |
|---------|-------------|------|
| `make docker-cache-warm` | Pre-load build cache | ~5-6 min |
| `make docker-build` | Build all images | ~2-5 min (warm cache) |
| `make docker-up` | Start all services | ~30 sec |
| `make docker-down` | Stop all services | ~10 sec |
| `make docker-logs` | View logs | - |
| `docker compose build api` | Build single service | ~30-60 sec (warm cache) |
## Troubleshooting
### "File exists (os error 17)" during build
Race condition detected. Solutions:
```bash
# Option 1: Clear cache and retry
docker builder prune -af
make docker-cache-warm
make docker-build
# Option 2: Build sequentially
docker compose build --no-parallel
```
### Builds are very slow
```bash
# Check cache size
docker system df -v | grep buildkit
# Prune if >20GB
docker builder prune --keep-storage 10GB
```
### Service won't start
```bash
# Check logs
docker compose logs api
# Restart single service
docker compose restart api
# Full restart
make docker-down
make docker-up
```
## Development Workflow
### Making Code Changes
```bash
# 1. Edit code
vim crates/api/src/routes/actions.rs
# 2. Rebuild affected service
docker compose build api
# 3. Restart it
docker compose up -d api
# 4. Check logs
docker compose logs -f api
```
### After Pulling Latest Code
```bash
# If dependencies changed (check Cargo.lock)
make docker-cache-warm
make docker-build
make docker-up
# If only code changed
make docker-build
make docker-up
```
## Environment Setup
Ensure BuildKit is enabled:
```bash
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
# Or add to ~/.bashrc or ~/.zshrc
echo 'export DOCKER_BUILDKIT=1' >> ~/.bashrc
echo 'export COMPOSE_DOCKER_CLI_BUILD=1' >> ~/.bashrc
```
## Architecture
```
Dockerfile (multi-stage)
├── Stage 1: Builder
│ ├── Install Rust + build deps
│ ├── Copy source code
│ ├── Build service (with cache mounts)
│ └── Extract binary
└── Stage 2: Runtime
├── Minimal Debian image
├── Copy binary from builder
├── Copy configs & migrations
└── Run service
Services built from same Dockerfile:
- api (port 8080)
- executor
- worker
- sensor
- notifier (port 8081)
Separate Dockerfile.web for React UI (port 3000)
```
## Cache Mounts Explained
| Mount | Purpose | Size | Sharing |
|-------|---------|------|---------|
| `/usr/local/cargo/registry` | Downloaded crates | ~1-2GB | locked |
| `/usr/local/cargo/git` | Git dependencies | ~100-500MB | locked |
| `/build/target` | Compiled artifacts | ~5-10GB | locked |
**`sharing=locked`** = Only one build at a time (prevents race conditions)
## Next Steps
- 📖 Read full details: [DOCKER_BUILD_RACE_CONDITIONS.md](./DOCKER_BUILD_RACE_CONDITIONS.md)
- 🐳 Docker configuration: [README.md](./README.md)
- 🚀 Quick start guide: [../docs/guides/quick-start.md](../docs/guides/quick-start.md)
## Questions?
- **Why is first build so slow?** Compiling Rust + all dependencies (~200+ crates)
- **Why cache warming?** Prevents multiple builds fighting over the same files
- **Can I build faster?** Yes, but with reliability trade-offs (see full docs)
- **Do I always need cache warming?** No, only for first build or dependency updates

View File

@@ -0,0 +1,253 @@
# Docker Build Race Conditions & Solutions
## Problem
When building multiple Attune services in parallel using `docker compose build`, you may encounter race conditions in the BuildKit cache mounts:
```
error: failed to unpack package `async-io v1.13.0`
Caused by:
failed to open `/usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-1.13.0/.cargo-ok`
Caused by:
File exists (os error 17)
```
**Root Cause**: Multiple Docker builds running in parallel try to extract the same Cargo dependencies into the shared cache mount (`/usr/local/cargo/registry`) simultaneously, causing file conflicts.
### Visual Explanation
**Without `sharing=locked` (Race Condition)**:
```
Time ──────────────────────────────────────────────>
Build 1 (API): [Download async-io] ──> [Extract .cargo-ok] ──> ❌ CONFLICT
Build 2 (Worker): [Download async-io] ──────> [Extract .cargo-ok] ──> ❌ CONFLICT
Build 3 (Executor): [Download async-io] ────────────> [Extract .cargo-ok] ──> ❌ CONFLICT
Build 4 (Sensor): [Download async-io] ──> [Extract .cargo-ok] ──────────────> ❌ CONFLICT
Build 5 (Notifier): [Download async-io] ────> [Extract .cargo-ok] ────────> ❌ CONFLICT
All trying to write to: /usr/local/cargo/registry/.../async-io-1.13.0/.cargo-ok
Result: "File exists (os error 17)"
```
**With `sharing=locked` (Sequential, Reliable)**:
```
Time ──────────────────────────────────────────────>
Build 1 (API): [Download + Extract] ──────────────> ✅ Success (~5 min)
Build 2 (Worker): [Build using cache] ──> ✅ Success (~5 min)
Build 3 (Executor): [Build using cache] ──> ✅ Success
Build 4 (Sensor): [Build] ──> ✅
Build 5 (Notifier): [Build] ──> ✅
Only one build accesses cache at a time
Result: 100% success, ~25-30 min total
```
**With Cache Warming (Optimized)**:
```
Time ──────────────────────────────────────────────>
Phase 1 - Warm:
Build 1 (API): [Download + Extract + Compile] ────> ✅ Success (~5-6 min)
Phase 2 - Parallel (cache already populated):
Build 2 (Worker): [Lock, compile, unlock] ──> ✅ Success
Build 3 (Executor): [Lock, compile, unlock] ────> ✅ Success
Build 4 (Sensor): [Lock, compile, unlock] ──────> ✅ Success
Build 5 (Notifier): [Lock, compile, unlock] ────────> ✅ Success
Result: 100% success, ~20-25 min total
```
## Solutions
### Solution 1: Use Locked Cache Sharing (Implemented)
The `Dockerfile` now uses `sharing=locked` on cache mounts, which ensures only one build can access the cache at a time:
```dockerfile
RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,target=/build/target,sharing=locked \
cargo build --release --bin attune-${SERVICE}
```
**Pros:**
- Reliable, no race conditions
- Simple configuration change
- No workflow changes needed
**Cons:**
- Services build sequentially (slower for fresh builds)
- First build takes ~25-30 minutes for all 5 services
### Solution 2: Pre-warm the Cache (Recommended Workflow)
Build one service first to populate the cache, then build the rest:
```bash
# Step 1: Warm the cache (builds API service only)
make docker-cache-warm
# Step 2: Build all services (much faster now)
make docker-build
```
Or manually:
```bash
docker compose build api # ~5-6 minutes
docker compose build # ~15-20 minutes for remaining services
```
**Why this works:**
- First build populates the shared Cargo registry cache
- Subsequent builds find dependencies already extracted
- Race condition risk is minimized (though not eliminated without `sharing=locked`)
### Solution 3: Sequential Build Script
Build services one at a time:
```bash
#!/bin/bash
for service in api executor worker sensor notifier web; do
echo "Building $service..."
docker compose build $service
done
```
**Pros:**
- No race conditions
- Predictable timing
**Cons:**
- Slower (can't leverage parallelism)
- ~25-30 minutes total for all services
### Solution 4: Disable Parallel Builds in docker compose
```bash
docker compose build --no-parallel
```
**Pros:**
- Simple one-liner
- No Dockerfile changes needed
**Cons:**
- Slower than Solution 2
- Less control over build order
## Recommended Workflow
For **first-time builds** or **after major dependency changes**:
```bash
make docker-cache-warm # Pre-load cache (~5-6 min)
make docker-build # Build remaining services (~15-20 min)
```
For **incremental builds** (code changes only):
```bash
make docker-build # ~2-5 minutes total with warm cache
```
For **single service rebuild**:
```bash
docker compose build api # Rebuild just the API
docker compose up -d api # Restart it
```
## Understanding BuildKit Cache Mounts
### What Gets Cached
1. **`/usr/local/cargo/registry`**: Downloaded crate archives (~1-2GB)
2. **`/usr/local/cargo/git`**: Git dependencies
3. **`/build/target`**: Compiled artifacts (~5-10GB per service)
### Cache Sharing Modes
- **`sharing=shared`** (default): Multiple builds can read/write simultaneously → race conditions
- **`sharing=locked`**: Only one build at a time → no races, but sequential
- **`sharing=private`**: Each build gets its own cache → no sharing benefits
### Why We Use `sharing=locked`
The trade-off between build speed and reliability favors reliability:
- **Without locking**: ~10-15 min (when it works), but fails ~30% of the time
- **With locking**: ~25-30 min consistently, never fails
The cache-warming workflow gives you the best of both worlds when needed.
## Troubleshooting
### "File exists" errors persist
1. Clear the build cache:
```bash
docker builder prune -af
```
2. Rebuild with cache warming:
```bash
make docker-cache-warm
make docker-build
```
### Builds are very slow
Check cache mount sizes:
```bash
docker system df -v | grep buildkit
```
If cache is huge (>20GB), consider pruning:
```bash
docker builder prune --keep-storage 10GB
```
### Want faster parallel builds
Remove `sharing=locked` from `docker/Dockerfile` and use cache warming:
```bash
# Edit docker/Dockerfile - remove ,sharing=locked from RUN --mount lines
make docker-cache-warm
make docker-build
```
**Warning**: This reintroduces race condition risk (~10-20% failure rate).
## Performance Comparison
| Method | First Build | Incremental | Reliability |
|--------|-------------|-------------|-------------|
| Parallel (no lock) | 10-15 min | 2-5 min | 70% success |
| Locked (current) | 25-30 min | 2-5 min | 100% success |
| Cache warm + build | 20-25 min | 2-5 min | 95% success |
| Sequential script | 25-30 min | 2-5 min | 100% success |
## References
- [BuildKit cache mounts documentation](https://docs.docker.com/build/cache/optimize/#use-cache-mounts)
- [Docker Compose build parallelization](https://docs.docker.com/compose/reference/build/)
- [Cargo concurrent download issues](https://github.com/rust-lang/cargo/issues/9719)
## Summary
**Current implementation**: Uses `sharing=locked` for guaranteed reliability.
**Recommended workflow**: Use `make docker-cache-warm` before `make docker-build` for faster initial builds.
**Trade-off**: Slight increase in build time (~5-10 min) for 100% reliability is worth it for production deployments.

145
docker/Dockerfile Normal file
View File

@@ -0,0 +1,145 @@
# Multi-stage Dockerfile for Attune Rust services
# This Dockerfile can build any of the Attune services by specifying a build argument
# Usage: DOCKER_BUILDKIT=1 docker build --build-arg SERVICE=api -f docker/Dockerfile -t attune-api .
#
# BuildKit cache mounts are used to speed up incremental builds by persisting:
# - Cargo registry and git cache (with sharing=locked to prevent race conditions)
# - Rust incremental compilation artifacts
#
# This dramatically reduces rebuild times from ~5 minutes to ~30 seconds for code-only changes.
ARG RUST_VERSION=1.92
ARG DEBIAN_VERSION=bookworm
# ============================================================================
# Stage 1: Builder - Compile the Rust services
# ============================================================================
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 manifests and source code
COPY Cargo.toml Cargo.lock ./
COPY crates/ ./crates/
COPY migrations/ ./migrations/
COPY .sqlx/ ./.sqlx/
# Build argument to specify which service to build
ARG SERVICE=api
# Build the specified service with BuildKit cache mounts
# Cache mount sharing modes prevent race conditions during parallel builds:
# - sharing=locked: Only one build can access the cache at a time (prevents file conflicts)
# - cargo registry/git: Locked to prevent "File exists" errors when extracting dependencies
# - target: Locked to prevent compilation artifact conflicts
#
# This is slower than parallel builds but eliminates race conditions.
# Alternative: Use docker-compose --build with --no-parallel flag, or build sequentially.
#
# First build: ~5-6 minutes
# Incremental builds (code changes only): ~30-60 seconds
RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,target=/build/target,sharing=locked \
cargo build --release --bin attune-${SERVICE} && \
cp /build/target/release/attune-${SERVICE} /build/attune-service-binary
# ============================================================================
# Stage 2: Pack Binaries Builder - Build native pack binaries with GLIBC 2.36
# ============================================================================
FROM rust:${RUST_VERSION}-${DEBIAN_VERSION} AS pack-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 files
COPY Cargo.toml Cargo.lock ./
COPY crates/ ./crates/
COPY .sqlx/ ./.sqlx/
# Build pack binaries (sensors, etc.) with GLIBC 2.36 for maximum compatibility
# These binaries will work on any system with GLIBC 2.36 or newer
# IMPORTANT: Copy binaries WITHIN the cache mount, before it's unmounted
RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,target=/build/target,sharing=locked \
mkdir -p /build/pack-binaries && \
cargo build --release --bin attune-core-timer-sensor && \
cp /build/target/release/attune-core-timer-sensor /build/pack-binaries/attune-core-timer-sensor && \
ls -lh /build/pack-binaries/
# Verify binaries were copied successfully (after cache unmount)
RUN ls -lah /build/pack-binaries/ && \
test -f /build/pack-binaries/attune-core-timer-sensor && \
echo "Timer sensor binary built successfully"
# ============================================================================
# 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
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
# Note: We copy from /build/attune-service-binary because the cache mount is not available in COPY
COPY --from=builder /build/attune-service-binary /usr/local/bin/attune-service
# Copy configuration files
COPY config.production.yaml ./config.yaml
COPY config.docker.yaml ./config.docker.yaml
# Copy migrations for services that need them
COPY migrations/ ./migrations/
# Copy packs directory (excluding binaries that will be overwritten)
COPY packs/ ./packs/
# Overwrite pack binaries with ones built with compatible GLIBC from pack-builder stage
# Copy individual files to ensure they overwrite existing ones
COPY --from=pack-builder /build/pack-binaries/attune-core-timer-sensor ./packs/core/sensors/attune-core-timer-sensor
# Make binaries executable and set ownership
RUN chmod +x ./packs/core/sensors/attune-core-timer-sensor && \
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.docker.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"]

View File

@@ -0,0 +1,102 @@
# Dockerfile for building Attune pack binaries with maximum GLIBC compatibility
#
# This builds native pack binaries (sensors, etc.) using an older GLIBC version
# to ensure forward compatibility across different deployment environments.
#
# GLIBC compatibility:
# - Binaries built with GLIBC 2.36 work on 2.36, 2.38, 2.39, etc. (forward compatible)
# - Binaries built with GLIBC 2.39 only work on 2.39+ (not backward compatible)
#
# Usage:
# docker build -f docker/Dockerfile.pack-builder -t attune-pack-builder .
# docker run --rm -v $(pwd)/packs:/output attune-pack-builder
#
# This will build all pack binaries and copy them to ./packs with GLIBC 2.36 compatibility
ARG RUST_VERSION=1.92
ARG DEBIAN_VERSION=bookworm
# ============================================================================
# Stage 1: Build Environment
# ============================================================================
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 files
COPY Cargo.toml Cargo.lock ./
COPY crates/ ./crates/
COPY .sqlx/ ./.sqlx/
# Build all pack binaries in release mode with BuildKit cache
RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,target=/build/target,sharing=locked \
cargo build --release --bin attune-core-timer-sensor && \
cargo build --release --bin attune-timer-sensor && \
mkdir -p /build/binaries && \
cp /build/target/release/attune-core-timer-sensor /build/binaries/ && \
cp /build/target/release/attune-timer-sensor /build/binaries/
# Verify GLIBC version used
RUN ldd --version | head -1 && \
echo "Binaries built with the above GLIBC version for maximum compatibility"
# ============================================================================
# Stage 2: Output Stage
# ============================================================================
FROM debian:${DEBIAN_VERSION}-slim AS output
WORKDIR /output
# Copy built binaries
COPY --from=builder /build/binaries/* ./
# Create a script to copy binaries to the correct pack locations
RUN cat > /copy-to-packs.sh << 'EOF'
#!/bin/bash
set -e
OUTPUT_DIR=${OUTPUT_DIR:-/output}
PACKS_DIR=${PACKS_DIR:-/packs}
echo "Copying pack binaries from /build to $PACKS_DIR..."
# Copy timer sensor binaries
if [ -f /build/attune-core-timer-sensor ]; then
mkdir -p "$PACKS_DIR/core/sensors"
cp /build/attune-core-timer-sensor "$PACKS_DIR/core/sensors/"
chmod +x "$PACKS_DIR/core/sensors/attune-core-timer-sensor"
echo "✓ Copied attune-core-timer-sensor to core pack"
fi
if [ -f /build/attune-timer-sensor ]; then
mkdir -p "$PACKS_DIR/core/sensors"
cp /build/attune-timer-sensor "$PACKS_DIR/core/sensors/"
chmod +x "$PACKS_DIR/core/sensors/attune-timer-sensor"
echo "✓ Copied attune-timer-sensor to core pack"
fi
# Verify GLIBC requirements
echo ""
echo "Verifying GLIBC compatibility..."
ldd /build/attune-core-timer-sensor 2>/dev/null | grep GLIBC || echo "Built with GLIBC $(ldd --version | head -1)"
echo ""
echo "Pack binaries built successfully with GLIBC 2.36 compatibility"
echo "These binaries will work on any system with GLIBC 2.36 or newer"
EOF
RUN chmod +x /copy-to-packs.sh
# Copy binaries to /build for the script
COPY --from=builder /build/binaries/* /build/
CMD ["/copy-to-packs.sh"]

54
docker/Dockerfile.web Normal file
View File

@@ -0,0 +1,54 @@
# Multi-stage Dockerfile for Attune Web UI
# Builds the React app and serves it with Nginx
ARG NODE_VERSION=20
ARG NGINX_VERSION=1.25-alpine
# ============================================================================
# Stage 1: Builder - Build the React application
# ============================================================================
FROM node:${NODE_VERSION}-alpine AS builder
WORKDIR /build
# Copy package files
COPY web/package.json web/package-lock.json ./
# Install dependencies
RUN npm ci --no-audit --prefer-offline
# Copy source code
COPY web/ ./
# Build the application
RUN npm run build
# ============================================================================
# Stage 2: Runtime - Serve with Nginx
# ============================================================================
FROM nginx:${NGINX_VERSION} AS runtime
# Remove default nginx config
RUN rm /etc/nginx/conf.d/default.conf
# Copy custom nginx configuration
COPY docker/nginx.conf /etc/nginx/conf.d/attune.conf
# Copy built assets from builder
COPY --from=builder /build/dist /usr/share/nginx/html
# Copy environment variable injection script
COPY docker/inject-env.sh /docker-entrypoint.d/40-inject-env.sh
RUN chmod +x /docker-entrypoint.d/40-inject-env.sh
# Create config directory for runtime config
RUN mkdir -p /usr/share/nginx/html/config
# Expose port 80
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
# Nginx will start automatically via the base image entrypoint

279
docker/Dockerfile.worker Normal file
View File

@@ -0,0 +1,279 @@
# Multi-stage Dockerfile for Attune workers
# Supports building different worker variants with different runtime capabilities
#
# Usage:
# docker build --target worker-base -t attune-worker:base -f docker/Dockerfile.worker .
# docker build --target worker-python -t attune-worker:python -f docker/Dockerfile.worker .
# docker build --target worker-node -t attune-worker:node -f docker/Dockerfile.worker .
# docker build --target worker-full -t attune-worker:full -f docker/Dockerfile.worker .
#
# BuildKit cache mounts are used to speed up incremental builds.
ARG RUST_VERSION=1.92
ARG DEBIAN_VERSION=bookworm
ARG PYTHON_VERSION=3.11
ARG NODE_VERSION=20
# ============================================================================
# Stage 1: 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 manifests and source code
COPY Cargo.toml Cargo.lock ./
COPY crates/ ./crates/
COPY migrations/ ./migrations/
COPY .sqlx/ ./.sqlx/
# Build the worker binary with BuildKit cache mounts
# sharing=locked prevents race conditions during parallel builds
RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,target=/build/target,sharing=locked \
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 2a: 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
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
# Copy packs directory
COPY packs/ ./packs/
# Set ownership
RUN chown -R attune:attune /opt/attune
# 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 2b: 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
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
# Copy packs directory
COPY packs/ ./packs/
# Set ownership
RUN chown -R attune:attune /opt/attune
# 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 2c: 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
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
# Copy packs directory
COPY packs/ ./packs/
# Set ownership
RUN chown -R attune:attune /opt/attune
# 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 2d: 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
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
# Copy packs directory
COPY packs/ ./packs/
# Set ownership
RUN chown -R attune:attune /opt/attune
# 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"]

229
docker/INIT-USER-README.md Normal file
View File

@@ -0,0 +1,229 @@
# Automatic User Initialization
This document explains how Attune automatically creates a default test user when running in Docker.
## Overview
When you start Attune with Docker Compose, a default test user is **automatically created** if it doesn't already exist. This eliminates the need for manual user setup during development and testing.
## Default Credentials
- **Login**: `test@attune.local`
- **Password**: `TestPass123!`
- **Display Name**: `Test User`
## How It Works
### Docker Compose Service Flow
```
1. postgres → Database starts
2. migrations → SQLx migrations run (creates schema and tables)
3. init-user → Creates default test user (if not exists)
4. api/workers/etc → Application services start
```
All application services depend on `init-user`, ensuring the test user exists before services start.
### Init Script: `init-user.sh`
The initialization script:
1. **Waits** for PostgreSQL to be ready
2. **Checks** if user `test@attune.local` already exists
3. **Creates** user if it doesn't exist (using pre-computed Argon2id hash)
4. **Skips** creation if user already exists (idempotent)
**Key Features:**
- ✅ Idempotent - safe to run multiple times
- ✅ Fast - uses pre-computed password hash
- ✅ Configurable via environment variables
- ✅ Automatic - no manual intervention needed
## Using the Default User
### Test Login via API
```bash
curl -X POST http://localhost:8080/auth/login \
-H 'Content-Type: application/json' \
-d '{"login":"test@attune.local","password":"TestPass123!"}'
```
**Successful Response:**
```json
{
"data": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "Bearer",
"expires_in": 86400,
"user": {
"id": 1,
"login": "test@attune.local",
"display_name": "Test User"
}
}
}
```
### Use Token for API Requests
```bash
# Get token
TOKEN=$(curl -s -X POST http://localhost:8080/auth/login \
-H 'Content-Type: application/json' \
-d '{"login":"test@attune.local","password":"TestPass123!"}' \
| jq -r '.data.access_token')
# Use token for authenticated request
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/v1/packs
```
## Customization
### Environment Variables
You can customize the default user by setting environment variables in `docker-compose.yaml`:
```yaml
init-user:
environment:
TEST_LOGIN: admin@company.com
TEST_PASSWORD: SuperSecure123!
TEST_DISPLAY_NAME: Administrator
```
### Custom Password Hash
For production or custom passwords, generate an Argon2id hash:
```bash
# Using Rust (requires project built)
cargo run --example hash_password "YourPasswordHere"
# Output: $argon2id$v=19$m=19456,t=2,p=1$...
```
Then update `init-user.sh` with your custom hash.
## Security Considerations
### Development vs Production
⚠️ **IMPORTANT SECURITY WARNINGS:**
1. **Default credentials are for development/testing ONLY**
- Never use `test@attune.local` / `TestPass123!` in production
- Disable or remove the `init-user` service in production deployments
2. **Change credentials before production**
- Set strong, unique passwords
- Use environment variables or secrets management
- Never commit credentials to version control
3. **Disable init-user in production**
```yaml
# In production docker-compose.override.yml
services:
init-user:
profiles: ["dev"] # Only runs with --profile dev
```
### Production User Creation
In production, create users via:
1. **Initial admin migration** - One-time database migration for bootstrap admin
2. **API registration endpoint** - If public registration is enabled
3. **Admin interface** - Web UI user management
4. **CLI tool** - `attune auth register` with proper authentication
## Troubleshooting
### User Creation Failed
**Symptom**: `init-user` container exits with error
**Check logs:**
```bash
docker-compose logs init-user
```
**Common issues:**
- Database not ready → Increase wait time or check database health
- Migration not complete → Verify `migrations` service completed successfully
- Schema mismatch → Ensure `DB_SCHEMA` matches your database configuration
### User Already Exists Error
This is **normal** and **expected** on subsequent runs. The script detects existing users and skips creation.
### Cannot Login with Default Credentials
**Verify user exists:**
```bash
docker-compose exec postgres psql -U attune -d attune \
-c "SELECT id, login, display_name FROM attune.identity WHERE login = 'test@attune.local';"
```
**Expected output:**
```
id | login | display_name
----+-------------------+--------------
1 | test@attune.local | Test User
```
**If user doesn't exist:**
```bash
# Recreate user by restarting init-user service
docker-compose up -d init-user
docker-compose logs -f init-user
```
### Wrong Password
If you customized `TEST_PASSWORD` but the login fails, you may need to regenerate the password hash. The default hash only works for `TestPass123!`.
## Files
- **`docker/init-user.sh`** - Initialization script
- **`docker-compose.yaml`** - Service definition for `init-user`
- **`docs/testing/test-user-setup.md`** - Detailed user setup guide
## Related Documentation
- [Test User Setup Guide](../docs/testing/test-user-setup.md) - Manual user creation
- [Docker README](./README.md) - Docker configuration overview
- [Production Deployment](../docs/deployment/production-deployment.md) - Production setup
## Quick Commands
```bash
# View init-user logs
docker-compose logs init-user
# Recreate default user
docker-compose restart init-user
# Check if user exists
docker-compose exec postgres psql -U attune -d attune \
-c "SELECT * FROM attune.identity WHERE login = 'test@attune.local';"
# Test login
curl -X POST http://localhost:8080/auth/login \
-H 'Content-Type: application/json' \
-d '{"login":"test@attune.local","password":"TestPass123!"}'
```
## Summary
-**Automatic**: User created on first startup
-**Idempotent**: Safe to run multiple times
-**Fast**: Uses pre-computed password hash
-**Configurable**: Customize via environment variables
-**Documented**: Clear credentials in comments and logs
- ⚠️ **Development only**: Not for production use
The automatic user initialization makes it easy to get started with Attune in Docker without manual setup steps!

303
docker/PORT_CONFLICTS.md Normal file
View File

@@ -0,0 +1,303 @@
# Docker Port Conflicts Resolution
## Problem
When starting Attune with Docker Compose, you may encounter port binding errors:
```
Error response from daemon: ports are not available: exposing port TCP 0.0.0.0:5432 -> 127.0.0.1:0: listen tcp 0.0.0.0:5432: bind: address already in use
```
This happens when **system-level services** (PostgreSQL, RabbitMQ, Redis) are already running and using the same ports that Docker containers need.
## Port Conflicts Table
| Service | Port | Docker Container | System Service |
|---------|------|------------------|----------------|
| PostgreSQL | 5432 | attune-postgres | postgresql |
| RabbitMQ (AMQP) | 5672 | attune-rabbitmq | rabbitmq-server |
| RabbitMQ (Management) | 15672 | attune-rabbitmq | rabbitmq-server |
| Redis | 6379 | attune-redis | redis-server |
| API | 8080 | attune-api | (usually free) |
| Notifier (WebSocket) | 8081 | attune-notifier | (usually free) |
| Web UI | 3000 | attune-web | (usually free) |
## Quick Fix
### Automated Script (Recommended)
Run the provided script to stop all conflicting services:
```bash
./scripts/stop-system-services.sh
```
This will:
1. Stop system PostgreSQL, RabbitMQ, and Redis services
2. Verify all ports are free
3. Clean up any orphaned Docker containers
4. Give you the option to disable services on boot
### Manual Fix
If the script doesn't work, follow these steps:
#### 1. Stop System PostgreSQL
```bash
# Check if running
systemctl is-active postgresql
# Stop it
sudo systemctl stop postgresql
# Optionally disable on boot
sudo systemctl disable postgresql
```
#### 2. Stop System RabbitMQ
```bash
# Check if running
systemctl is-active rabbitmq-server
# Stop it
sudo systemctl stop rabbitmq-server
# Optionally disable on boot
sudo systemctl disable rabbitmq-server
```
#### 3. Stop System Redis
```bash
# Check if running
systemctl is-active redis-server
# Stop it
sudo systemctl stop redis-server
# Optionally disable on boot
sudo systemctl disable redis-server
```
#### 4. Verify Ports are Free
```bash
# Check PostgreSQL port
nc -zv localhost 5432
# Check RabbitMQ port
nc -zv localhost 5672
# Check Redis port
nc -zv localhost 6379
# All should return "Connection refused" (meaning free)
```
## Finding What's Using a Port
If ports are still in use after stopping services:
```bash
# Method 1: Using lsof (most detailed)
sudo lsof -i :5432
# Method 2: Using ss
sudo ss -tulpn | grep 5432
# Method 3: Using netstat
sudo netstat -tulpn | grep 5432
# Method 4: Using fuser
sudo fuser 5432/tcp
```
## Killing a Process on a Port
```bash
# Find the process ID
PID=$(lsof -ti tcp:5432)
# Kill it
sudo kill $PID
# Force kill if needed
sudo kill -9 $PID
```
## Docker-Specific Issues
### Orphaned Containers
Sometimes Docker containers remain running after a failed `docker compose down`:
```bash
# List all containers (including stopped)
docker ps -a
# Stop and remove Attune containers
docker compose down
# Remove orphaned containers using specific ports
docker ps -q --filter "publish=5432" | xargs docker stop
docker ps -q --filter "publish=5672" | xargs docker stop
docker ps -q --filter "publish=6379" | xargs docker stop
```
### Corrupted Container in Restart Loop
If `docker ps -a` shows a container with status "Restarting (255)":
```bash
# Check logs
docker logs attune-postgres
# If you see "exec format error", the image is corrupted
docker compose down
docker rmi postgres:16-alpine
docker volume rm attune_postgres_data
docker pull postgres:16-alpine
docker compose up -d
```
## Alternative: Change Docker Ports
If you want to keep system services running, modify `docker compose.yaml` to use different ports:
```yaml
postgres:
ports:
- "5433:5432" # Map to 5433 on host instead
rabbitmq:
ports:
- "5673:5672" # Map to 5673 on host instead
- "15673:15672"
redis:
ports:
- "6380:6379" # Map to 6380 on host instead
```
Then update your config files to use these new ports:
```yaml
# config.docker.yaml
database:
url: postgresql://attune:attune@postgres:5432 # Internal still uses 5432
# But if accessing from host:
database:
url: postgresql://attune:attune@localhost:5433 # Use external port
```
## Recommended Approach for Development
**Option 1: Use Docker Exclusively (Recommended)**
Stop all system services and use Docker for everything:
```bash
./scripts/stop-system-services.sh
docker compose up -d
```
**Pros:**
- Clean separation from system
- Easy to start/stop all services together
- Consistent with production deployment
- No port conflicts
**Cons:**
- Need to use Docker commands to access services
- Slightly more overhead
**Option 2: Use System Services**
Don't use Docker Compose, run services directly:
```bash
sudo systemctl start postgresql
sudo systemctl start rabbitmq-server
sudo systemctl start redis-server
# Then run Attune services natively
cargo run --bin attune-api
cargo run --bin attune-executor
# etc.
```
**Pros:**
- Familiar system tools
- Easier debugging with local tools
- Lower overhead
**Cons:**
- Manual service management
- Different from production
- Version mismatches possible
## Re-enabling System Services
To go back to using system services:
```bash
# Start and enable services
sudo systemctl start postgresql
sudo systemctl start rabbitmq-server
sudo systemctl start redis-server
sudo systemctl enable postgresql
sudo systemctl enable rabbitmq-server
sudo systemctl enable redis-server
```
## Troubleshooting Checklist
- [ ] Stop Docker containers: `docker compose down`
- [ ] Stop system PostgreSQL: `sudo systemctl stop postgresql`
- [ ] Stop system RabbitMQ: `sudo systemctl stop rabbitmq-server`
- [ ] Stop system Redis: `sudo systemctl stop redis-server`
- [ ] Verify port 5432 is free: `nc -zv localhost 5432`
- [ ] Verify port 5672 is free: `nc -zv localhost 5672`
- [ ] Verify port 6379 is free: `nc -zv localhost 6379`
- [ ] Check for orphaned containers: `docker ps -a | grep attune`
- [ ] Check for corrupted images: `docker logs attune-postgres`
- [ ] Start fresh: `docker compose up -d`
## Prevention
To avoid this issue in the future:
1. **Add to your shell profile** (`~/.bashrc` or `~/.zshrc`):
```bash
alias attune-docker-start='cd /path/to/attune && ./scripts/stop-system-services.sh && docker compose up -d'
alias attune-docker-stop='cd /path/to/attune && docker compose down'
alias attune-docker-logs='cd /path/to/attune && docker compose logs -f'
```
2. **Create a systemd service** to automatically stop conflicting services when starting Docker:
See `docs/deployment/systemd-setup.md` for details (if available).
3. **Use different ports** as described above to run both simultaneously.
## Summary
The most reliable approach:
```bash
# One-time setup
./scripts/stop-system-services.sh
# Answer 'y' to disable services on boot
# Then use Docker
docker compose up -d # Start all services
docker compose logs -f # View logs
docker compose down # Stop all services
```
This ensures clean, reproducible environments that match production deployment.

485
docker/QUICKREF.md Normal file
View File

@@ -0,0 +1,485 @@
# Docker Quick Reference
Quick reference for common Docker commands when working with Attune.
## Table of Contents
- [Quick Start](#quick-start)
- [Service Management](#service-management)
- [Viewing Logs](#viewing-logs)
- [Database Operations](#database-operations)
- [Debugging](#debugging)
- [Maintenance](#maintenance)
- [Troubleshooting](#troubleshooting)
- [BuildKit Cache](#buildkit-cache)
## Quick Start
**Enable BuildKit first (recommended):**
```bash
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
```
```bash
# One-command setup (generates secrets, builds, starts)
./docker/quickstart.sh
# Manual setup
cp env.docker.example .env
# Edit .env and set JWT_SECRET and ENCRYPTION_KEY
docker compose up -d
```
## Service Management
### Start/Stop Services
```bash
# Start all services
docker compose up -d
# Start specific services
docker compose up -d postgres rabbitmq redis
docker compose up -d api executor worker
# Stop all services
docker compose down
# Stop and remove volumes (WARNING: deletes data)
docker compose down -v
# Restart services
docker compose restart
# Restart specific service
docker compose restart api
```
### Build Images
```bash
# Build all images
docker compose build
# Build specific service
docker compose build api
docker compose build web
# Build without cache (clean build)
docker compose build --no-cache
# Build with BuildKit (faster incremental builds)
DOCKER_BUILDKIT=1 docker compose build
# Pull latest base images and rebuild
docker compose build --pull
```
### Scale Services
```bash
# Run multiple worker instances
docker compose up -d --scale worker=3
# Run multiple executor instances
docker compose up -d --scale executor=2
```
## Viewing Logs
```bash
# View all logs (follow mode)
docker compose logs -f
# View specific service logs
docker compose logs -f api
docker compose logs -f worker
docker compose logs -f postgres
# View last N lines
docker compose logs --tail=100 api
# View logs since timestamp
docker compose logs --since 2024-01-01T10:00:00 api
# View logs without following
docker compose logs api
```
## Database Operations
### Access PostgreSQL
```bash
# Connect to database
docker compose exec postgres psql -U attune
# Run SQL query
docker compose exec postgres psql -U attune -c "SELECT COUNT(*) FROM attune.execution;"
# List tables
docker compose exec postgres psql -U attune -c "\dt attune.*"
# Describe table
docker compose exec postgres psql -U attune -c "\d attune.execution"
```
### Backup and Restore
```bash
# Backup database
docker compose exec postgres pg_dump -U attune > backup.sql
# Restore database
docker compose exec -T postgres psql -U attune < backup.sql
# Backup specific table
docker compose exec postgres pg_dump -U attune -t attune.execution > executions_backup.sql
```
### Run Migrations
```bash
# Check migration status
docker compose exec api sqlx migrate info
# Run pending migrations
docker compose exec api sqlx migrate run
# Revert last migration
docker compose exec api sqlx migrate revert
```
## Debugging
### Access Service Shell
```bash
# API service
docker compose exec api /bin/sh
# Worker service
docker compose exec worker /bin/sh
# Database
docker compose exec postgres /bin/bash
```
### Check Service Status
```bash
# View running services and health
docker compose ps
# View detailed container info
docker inspect attune-api
# View resource usage
docker stats
# View container processes
docker compose top
```
### Test Connections
```bash
# Test API health
curl http://localhost:8080/health
# Test from inside container
docker compose exec worker curl http://api:8080/health
# Test database connection
docker compose exec api sh -c 'psql postgresql://attune:attune@postgres:5432/attune -c "SELECT 1"'
# Test RabbitMQ
docker compose exec rabbitmq rabbitmqctl status
docker compose exec rabbitmq rabbitmqctl list_queues
```
### View Configuration
```bash
# View environment variables
docker compose exec api env
# View config file
docker compose exec api cat /opt/attune/config.docker.yaml
# View generated docker compose config
docker compose config
```
## Maintenance
### Update Images
```bash
# Pull latest base images
docker compose pull
# Rebuild with latest bases
docker compose build --pull
# Restart with new images
docker compose up -d
```
### Clean Up
```bash
# Remove stopped containers
docker compose down
# Remove volumes (deletes data)
docker compose down -v
# Remove images
docker compose down --rmi local
# Prune unused Docker resources
docker system prune -f
# Prune everything including volumes
docker system prune -a --volumes
```
### View Disk Usage
```bash
# Docker disk usage summary
docker system df
# Detailed breakdown
docker system df -v
# Volume sizes
docker volume ls -q | xargs docker volume inspect --format '{{ .Name }}: {{ .Mountpoint }}'
```
## Troubleshooting
### Service Won't Start
```bash
# Check logs for errors
docker compose logs <service-name>
# Check if dependencies are healthy
docker compose ps
# Verify configuration
docker compose config --quiet
# Try rebuilding
docker compose build --no-cache <service-name>
docker compose up -d <service-name>
```
### Database Connection Issues
```bash
# Verify PostgreSQL is running
docker compose ps postgres
# Check PostgreSQL logs
docker compose logs postgres
# Test connection
docker compose exec postgres pg_isready -U attune
# Check network
docker compose exec api ping postgres
```
### RabbitMQ Issues
```bash
# Check RabbitMQ status
docker compose exec rabbitmq rabbitmqctl status
# Check queues
docker compose exec rabbitmq rabbitmqctl list_queues
# Check connections
docker compose exec rabbitmq rabbitmqctl list_connections
# Access management UI
open http://localhost:15672
```
### Permission Errors
```bash
# Fix volume permissions (UID 1000 = attune user)
sudo chown -R 1000:1000 ./packs
sudo chown -R 1000:1000 ./logs
# Check current permissions
ls -la ./packs
ls -la ./logs
```
### Network Issues
```bash
# List networks
docker network ls
# Inspect attune network
docker network inspect attune_attune-network
# Test connectivity between services
docker compose exec api ping postgres
docker compose exec api ping rabbitmq
docker compose exec api ping redis
```
### Reset Everything
```bash
# Nuclear option - complete reset
docker compose down -v --rmi all
docker system prune -a --volumes
rm -rf ./logs/*
# Then rebuild
./docker/quickstart.sh
```
## Environment Variables
Override configuration with environment variables:
```bash
# In .env file or export
export ATTUNE__DATABASE__URL=postgresql://user:pass@host:5432/db
export ATTUNE__LOG__LEVEL=debug
export RUST_LOG=trace
# Then restart services
docker compose up -d
```
## Useful Aliases
Add to `~/.bashrc` or `~/.zshrc`:
```bash
alias dc='docker compose'
alias dcu='docker compose up -d'
alias dcd='docker compose down'
alias dcl='docker compose logs -f'
alias dcp='docker compose ps'
alias dcr='docker compose restart'
# Attune-specific
alias attune-logs='docker compose logs -f api executor worker sensor'
alias attune-db='docker compose exec postgres psql -U attune'
alias attune-shell='docker compose exec api /bin/sh'
```
## Makefile Commands
Project Makefile includes Docker shortcuts:
```bash
make docker-build # Build all images
make docker-up # Start services
make docker-down # Stop services
make docker-logs # View logs
make docker-ps # View status
make docker-shell-api # Access API shell
make docker-shell-db # Access database
make docker-clean # Clean up resources
```
## BuildKit Cache
### Enable BuildKit
BuildKit dramatically speeds up incremental builds (5+ minutes → 30-60 seconds):
```bash
# Enable for current session
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
# Enable globally
./docker/enable-buildkit.sh
# Or manually add to ~/.bashrc or ~/.zshrc
echo 'export DOCKER_BUILDKIT=1' >> ~/.bashrc
echo 'export COMPOSE_DOCKER_CLI_BUILD=1' >> ~/.bashrc
source ~/.bashrc
```
### Manage Build Cache
```bash
# View cache size
docker system df
# View detailed cache info
docker system df -v
# Clear build cache
docker builder prune
# Clear all unused cache
docker builder prune -a
# Clear specific cache type
docker builder prune --filter type=exec.cachemount
```
### Cache Performance
**With BuildKit:**
- First build: ~5-6 minutes
- Code-only changes: ~30-60 seconds
- Dependency changes: ~2-3 minutes
- Cache size: ~5-10GB
**Without BuildKit:**
- Every build: ~5-6 minutes (no incremental compilation)
### Verify BuildKit is Working
```bash
# Check environment
echo $DOCKER_BUILDKIT
# Test BuildKit with cache mounts
cat > /tmp/test.Dockerfile <<EOF
FROM alpine:latest
RUN --mount=type=cache,target=/cache echo "BuildKit works!"
EOF
DOCKER_BUILDKIT=1 docker build -f /tmp/test.Dockerfile /tmp
# If successful, BuildKit is working
```
## Production Checklist
Before deploying to production:
- [ ] Generate secure `JWT_SECRET` and `ENCRYPTION_KEY`
- [ ] Change all default passwords (database, RabbitMQ)
- [ ] Configure proper `CORS_ORIGINS`
- [ ] Set up TLS/SSL with reverse proxy
- [ ] Configure persistent volumes with backups
- [ ] Set up log aggregation
- [ ] Configure monitoring and alerting
- [ ] Review resource limits
- [ ] Test backup/restore procedures
- [ ] Document incident response procedures
## Additional Resources
- [Full Docker Deployment Guide](../docs/docker-deployment.md)
- [Docker Directory README](README.md)
- [Production Deployment](../docs/production-deployment.md)
- [Configuration Guide](../docs/configuration.md)

428
docker/QUICK_START.md Normal file
View File

@@ -0,0 +1,428 @@
# Attune Docker Quick Start Guide
Get Attune running in Docker in under 5 minutes!
## Prerequisites
- Docker Engine 20.10+ (with BuildKit support)
- Docker Compose V2 (included with Docker Desktop)
- 4GB+ available RAM
- Ports available: 3000, 5432, 5672, 6379, 8080, 8081, 15672
## TL;DR - Fastest Path
```bash
# Clone and enter directory
cd /path/to/attune
# Stop conflicting system services (if needed)
./scripts/stop-system-services.sh
# Start everything
docker compose up -d
# Check status
docker compose ps
# Access the UI
open http://localhost:3000
```
## Step-by-Step Setup
### 1. Prepare Your Environment
#### Stop System Services (if running)
If you have PostgreSQL, RabbitMQ, or Redis running on your system:
```bash
./scripts/stop-system-services.sh
```
This will:
- Stop PostgreSQL (port 5432)
- Stop RabbitMQ (ports 5672, 15672)
- Stop Redis (port 6379)
- Verify ports are free
- Optionally disable services on boot
**Alternative**: If you want to keep system services, see [PORT_CONFLICTS.md](./PORT_CONFLICTS.md) for changing Docker ports.
### 2. Start Attune
```bash
docker compose up -d
```
**What happens**:
1. Downloads Docker images (first time: ~5-10 min)
2. Creates network and volumes
3. Starts PostgreSQL, RabbitMQ, Redis
4. Runs database migrations automatically (16 migrations)
5. Starts Attune services (API, Executor, Worker, Sensor, Notifier)
6. Starts Web UI
### 3. Verify Services
```bash
# Check all services
docker compose ps
# Expected output - all should be "Up" or "Up (healthy)":
# NAME STATUS
# attune-api Up (healthy)
# attune-executor Up (healthy)
# attune-notifier Up (healthy)
# attune-sensor Up (healthy)
# attune-web Up (healthy)
# attune-worker Up (healthy) # May restart if Python missing
# attune-postgres Up (healthy)
# attune-rabbitmq Up (healthy)
# attune-redis Up (healthy)
```
### 4. Access Services
| Service | URL | Purpose |
|---------|-----|---------|
| Web UI | http://localhost:3000 | Main interface |
| API | http://localhost:8080 | REST API |
| API Docs | http://localhost:8080/api-docs | Interactive API documentation |
| Health Check | http://localhost:8080/health | Service status |
| RabbitMQ Management | http://localhost:15672 | Queue monitoring (attune/attune) |
```bash
# Test API
curl http://localhost:8080/health
# Open Web UI
open http://localhost:3000 # macOS
xdg-open http://localhost:3000 # Linux
```
## Next Steps
### Create Your First User
```bash
# Option 1: Use the CLI (recommended)
docker compose exec api attune-service create-admin-user \
--username admin \
--email admin@example.com \
--password changeme
# Option 2: Use the Web UI registration page
open http://localhost:3000/register
```
### Load Core Pack (Optional)
The core pack provides basic actions (HTTP requests, timers, etc.):
```bash
./scripts/load-core-pack.sh
```
### Explore the API
```bash
# Get JWT token
TOKEN=$(curl -s -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"changeme"}' \
| jq -r '.access_token')
# List packs
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/v1/packs
# List actions
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/v1/actions
```
## Common Commands
### Managing Services
```bash
# Start all services
docker compose up -d
# Stop all services
docker compose down
# Restart a specific service
docker compose restart api
# View logs
docker compose logs -f # All services
docker compose logs -f api # Single service
docker compose logs --tail 100 api # Last 100 lines
# Check service status
docker compose ps
```
### Database Operations
```bash
# Access PostgreSQL
docker compose exec postgres psql -U attune -d attune
# View applied migrations
docker compose exec postgres psql -U attune -d attune \
-c "SELECT * FROM _migrations ORDER BY applied_at;"
# Reset database (WARNING: Deletes all data)
docker compose down -v
docker compose up -d
```
### Troubleshooting
```bash
# View service logs
docker compose logs api --tail 50
# Check migrations
docker compose logs migrations
# Restart everything
docker compose restart
# Full reset (deletes all data)
docker compose down -v
docker compose up -d
```
## Troubleshooting
### Port Already in Use
**Error**: `address already in use`
**Solution**:
```bash
./scripts/stop-system-services.sh
```
Or see [PORT_CONFLICTS.md](./PORT_CONFLICTS.md) for alternatives.
### Services Keep Restarting
**Check logs**:
```bash
docker compose logs api --tail 20
```
**Common issues**:
- Database not ready → Wait 30 seconds, should auto-recover
- Configuration error → Check environment variables
- Migration failed → Check `docker compose logs migrations`
**Fix**:
```bash
docker compose down
docker compose up -d
```
### Migrations Failed
**View migration logs**:
```bash
docker compose logs migrations
```
**Reset and retry**:
```bash
docker compose down -v # Deletes database
docker compose up -d
```
### Worker Service Failing
**Error**: `Python validation failed`
**Cause**: Worker container doesn't have Python installed
**Solution**: This is non-critical. Worker will be fixed in future update. Core services work fine without it.
### Can't Access Web UI
**Check**:
```bash
docker compose ps web
docker compose logs web --tail 20
```
**Try**:
```bash
docker compose restart web
open http://localhost:3000
```
## Configuration
### Environment Variables
Create a `.env` file in the project root to customize settings:
```bash
cp env.docker.example .env
```
Edit `.env`:
```bash
# Security (REQUIRED for production)
JWT_SECRET=your-random-secret-here
ENCRYPTION_KEY=your-32-plus-character-encryption-key-here
# Ports (optional)
API_PORT=8080
WEB_PORT=3000
POSTGRES_PORT=5432
```
### Custom Configuration
Override default config with environment variables:
```bash
# Format: ATTUNE__SECTION__KEY=value
export ATTUNE__SECURITY__JWT_SECRET=my-secret
export ATTUNE__DATABASE__URL=postgresql://custom:url@host:5432/db
export ATTUNE__WORKER__WORKER_TYPE=container
```
See [configuration.md](../docs/configuration/configuration.md) for all options.
## Stopping Attune
### Keep Data (Recommended)
```bash
docker compose down
```
Volumes persist, data retained. Next `docker compose up -d` restarts with existing data.
### Delete Everything
```bash
docker compose down -v # WARNING: Deletes all data
```
Removes all volumes, containers, and networks. Next startup is fresh install.
## Production Deployment
This quick start is for **development only**. For production:
1. **Generate secure secrets**:
```bash
openssl rand -base64 32 # JWT_SECRET
openssl rand -base64 32 # ENCRYPTION_KEY
```
2. **Use proper .env file** with strong credentials
3. **Configure HTTPS** with reverse proxy (nginx, Traefik)
4. **Set up backups** for PostgreSQL volumes
5. **Use external database** (recommended) instead of containerized
6. **Monitor with logging/metrics** (Prometheus, Grafana)
See [production-deployment.md](../docs/deployment/production-deployment.md) for details.
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Docker Compose │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Web UI │ │ API │ │ Executor │ │
│ │ (React) │ │ (Rust) │ │ (Rust) │ │
│ │ Port 3000 │ │ Port 8080 │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Worker │ │ Sensor │ │ Notifier │ │
│ │ (Rust) │ │ (Rust) │ │ (Rust) │ │
│ │ │ │ │ │ Port 8081 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ──────────────── Infrastructure ─────────────────── │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ PostgreSQL │ │ RabbitMQ │ │ Redis │ │
│ │ Port 5432 │ │ Port 5672 │ │ Port 6379 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Migrations (runs once at startup) │ │
│ └────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
## Resources
- [Docker Documentation](./README.md)
- [Port Conflicts Guide](./PORT_CONFLICTS.md)
- [Build Race Conditions](./DOCKER_BUILD_RACE_CONDITIONS.md)
- [Production Deployment](../docs/deployment/production-deployment.md)
- [Configuration Guide](../docs/configuration/configuration.md)
## Getting Help
- **View logs**: `docker compose logs <service>`
- **Check documentation**: `docs/` directory
- **API documentation**: http://localhost:8080/api-docs (when running)
- **Report issues**: GitHub issues
## Quick Reference
### Essential Commands
```bash
# Start
docker compose up -d
# Stop
docker compose down
# Logs
docker compose logs -f
# Status
docker compose ps
# Restart
docker compose restart
# Reset (deletes data)
docker compose down -v && docker compose up -d
```
### Service URLs
- Web UI: http://localhost:3000
- API: http://localhost:8080
- API Docs: http://localhost:8080/api-docs
- RabbitMQ: http://localhost:15672 (attune/attune)
### Default Credentials
- PostgreSQL: `attune` / `attune`
- RabbitMQ: `attune` / `attune`
- First user: Create via CLI or Web UI
---
**Ready to automate?** Start building workflows in the Web UI at http://localhost:3000! 🚀

560
docker/README.md Normal file
View File

@@ -0,0 +1,560 @@
# Attune Docker Configuration
This directory contains Docker-related files for building and running Attune services.
> **⚠️ Important**: When building multiple services in parallel, you may encounter race conditions. See [DOCKER_BUILD_RACE_CONDITIONS.md](./DOCKER_BUILD_RACE_CONDITIONS.md) for solutions and recommended workflows.
## Quick Start
### Default User Credentials
When you start Attune with Docker Compose, a default test user is **automatically created**:
- **Login**: `test@attune.local`
- **Password**: `TestPass123!`
This happens via the `init-user` service which runs after database migrations complete.
### Test Login
```bash
curl -X POST http://localhost:8080/auth/login \
-H 'Content-Type: application/json' \
-d '{"login":"test@attune.local","password":"TestPass123!"}'
```
> **⚠️ Security Note**: This default user is for development/testing only. Never use these credentials in production!
## Files
### Dockerfiles
- **`Dockerfile`** - Multi-stage Dockerfile for all Rust services (API, Executor, Worker, Sensor, Notifier)
- Uses build argument `SERVICE` to specify which service to build
- Example: `docker build --build-arg SERVICE=api -f docker/Dockerfile -t attune-api .`
- **`Dockerfile.worker`** - Multi-stage Dockerfile for containerized workers with different runtime capabilities
- Supports 4 variants: `worker-base`, `worker-python`, `worker-node`, `worker-full`
- See [README.worker.md](./README.worker.md) for details
- **`Dockerfile.web`** - Multi-stage Dockerfile for React Web UI
- Builds with Node.js and serves with Nginx
- Includes runtime environment variable injection
### Configuration Files
- **`nginx.conf`** - Nginx configuration for serving Web UI and proxying API/WebSocket requests
- Serves static React assets
- Proxies `/api/*` to API service
- Proxies `/ws/*` to Notifier service (WebSocket)
- Includes security headers and compression
- **`inject-env.sh`** - Script to inject runtime environment variables into Web UI
- Runs at container startup
- Creates `runtime-config.js` with API and WebSocket URLs
### Initialization Scripts
- **`init-db.sh`** - Database initialization script (optional)
- Waits for PostgreSQL readiness
- Creates schema and runs migrations
- Can be used for manual DB setup
- **`init-user.sh`** - Default user initialization script
- **Automatically creates** test user on first startup
- Idempotent - safe to run multiple times
- Creates user: `test@attune.local` / `TestPass123!`
- Uses pre-computed Argon2id password hash
- Skips creation if user already exists
- **`run-migrations.sh`** - Database migration runner
- Runs SQLx migrations automatically on startup
- Used by the `migrations` service in docker-compose
### Docker Compose
The main `docker compose.yaml` is in the project root. It orchestrates:
- Infrastructure: PostgreSQL, RabbitMQ, Redis
- Services: API, Executor, Worker, Sensor, Notifier
- Web UI: React frontend with Nginx
## Building Images
### Build All Services (Recommended Method)
To avoid race conditions during parallel builds, pre-warm the cache first:
```bash
cd /path/to/attune
# Enable BuildKit for faster incremental builds (recommended)
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
# Step 1: Pre-warm the build cache (builds API service only)
make docker-cache-warm
# Step 2: Build all services (faster and more reliable)
make docker-build
```
Or build directly with docker compose:
```bash
docker compose build
```
**Note**: The Dockerfile uses `sharing=locked` on cache mounts to prevent race conditions. This makes parallel builds sequential but ensures 100% reliability. See [DOCKER_BUILD_RACE_CONDITIONS.md](./DOCKER_BUILD_RACE_CONDITIONS.md) for details.
### Build Individual Service
```bash
# Enable BuildKit first
export DOCKER_BUILDKIT=1
# API service
docker compose build api
# Web UI
docker compose build web
# Worker service
docker compose build worker
```
### Build with Custom Args
```bash
# Build API with specific Rust version
DOCKER_BUILDKIT=1 docker build \
--build-arg SERVICE=api \
--build-arg RUST_VERSION=1.92 \
-f docker/Dockerfile \
-t attune-api:custom \
.
```
### Enable BuildKit Globally
BuildKit dramatically speeds up incremental builds by caching compilation artifacts.
```bash
# Run the configuration script
./docker/enable-buildkit.sh
# Or manually add to your shell profile (~/.bashrc, ~/.zshrc, etc.)
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
# Apply changes
source ~/.bashrc # or ~/.zshrc
```
## Image Structure
### Rust Services
**Builder Stage:**
- Base: `rust:1.92-bookworm`
- Installs build dependencies
- Compiles the specified service in release mode
- **Uses BuildKit cache mounts for incremental builds**
- Build time:
- First build: ~5-6 minutes
- Incremental builds (with BuildKit): ~30-60 seconds
- Without BuildKit: ~5-6 minutes every time
**Runtime Stage:**
- Base: `debian:bookworm-slim`
- Minimal runtime dependencies (ca-certificates, libssl3, curl)
- Runs as non-root user `attune` (UID 1000)
- Binary copied to `/usr/local/bin/attune-service`
- Configuration in `/opt/attune/`
- Packs directory: `/opt/attune/packs`
### Web UI
**Builder Stage:**
- Base: `node:20-alpine`
- Installs npm dependencies
- Builds React application with Vite
**Runtime Stage:**
- Base: `nginx:1.25-alpine`
- Custom Nginx configuration
- Static files in `/usr/share/nginx/html`
- Environment injection script at startup
## Environment Variables
Rust services support environment-based configuration with `ATTUNE__` prefix:
```bash
# Database
ATTUNE__DATABASE__URL=postgresql://user:pass@host:5432/db
# Message Queue
ATTUNE__MESSAGE_QUEUE__URL=amqp://user:pass@host:5672
# Security
JWT_SECRET=your-secret-here
ENCRYPTION_KEY=your-32-char-key-here
# Logging
RUST_LOG=debug
```
Web UI environment variables:
```bash
API_URL=http://localhost:8080
WS_URL=ws://localhost:8081
ENVIRONMENT=production
```
## Volumes
The following volumes are used:
**Data Volumes:**
- `postgres_data` - PostgreSQL database files
- `rabbitmq_data` - RabbitMQ data
- `redis_data` - Redis persistence
**Log Volumes:**
- `api_logs`, `executor_logs`, `worker_logs`, `sensor_logs`, `notifier_logs`
**Temporary:**
- `worker_temp` - Worker service temporary files
**Bind Mounts:**
- `./packs:/opt/attune/packs:ro` - Read-only pack files
## Networking
All services run on the `attune-network` bridge network with subnet `172.28.0.0/16`.
**Service Communication:**
- Services communicate using container names as hostnames
- Example: API connects to `postgres:5432`, `rabbitmq:5672`
**External Access:**
- API: `localhost:8080`
- Notifier WebSocket: `localhost:8081`
- Web UI: `localhost:3000`
- RabbitMQ Management: `localhost:15672`
- PostgreSQL: `localhost:5432`
## Health Checks
All services have health checks configured:
**API:**
```bash
curl -f http://localhost:8080/health
```
**Web UI:**
```bash
wget --spider http://localhost:80/health
```
**PostgreSQL:**
```bash
pg_isready -U attune
```
**RabbitMQ:**
```bash
rabbitmq-diagnostics -q ping
```
**Background Services:**
Process existence check with `pgrep`
## Security
### Non-Root User
All services run as non-root user `attune` (UID 1000) for security.
### Secrets Management
**Development:**
- Secrets in `.env` file (not committed to git)
- Default values for testing only
**Production:**
- Use Docker secrets or external secrets manager
- Never hardcode secrets in images
- Rotate secrets regularly
### Network Security
- Services isolated on private network
- Only necessary ports exposed to host
- Use TLS/SSL for external connections
- Dedicated bridge network
- Proper startup dependencies
## Optimization
### BuildKit Cache Mounts (Recommended)
**Enable BuildKit** for dramatically faster incremental builds:
```bash
export DOCKER_BUILDKIT=1
docker compose build
```
**How it works:**
- Persists `/usr/local/cargo/registry` (downloaded crates, ~1-2GB)
- Persists `/usr/local/cargo/git` (git dependencies)
- Persists `/build/target` (compilation artifacts, ~5-10GB)
**Performance improvement:**
- First build: ~5-6 minutes
- Code-only changes: ~30-60 seconds (vs 5+ minutes without caching)
- Dependency changes: ~2-3 minutes (vs full rebuild)
**Manage cache:**
```bash
# View cache size
docker system df
# Clear build cache
docker builder prune
# Clear specific cache
docker builder prune --filter type=exec.cachemount
```
### Layer Caching
The Dockerfiles are also optimized for Docker layer caching:
1. Copy manifests first
2. Download and compile dependencies
3. Copy actual source code last
4. Source code changes don't invalidate dependency layers
### Image Size
**Rust Services:**
- Multi-stage build reduces size
- Only runtime dependencies in final image
- Typical size: 140-180MB per service
**Web UI:**
- Static files only in final image
- Alpine-based Nginx
- Typical size: 50-80MB
### Build Time
**With BuildKit (recommended):**
- First build: ~5-6 minutes
- Code-only changes: ~30-60 seconds
- Dependency changes: ~2-3 minutes
**Without BuildKit:**
- Every build: ~5-6 minutes
**Enable BuildKit:**
```bash
./docker/enable-buildkit.sh
# or
export DOCKER_BUILDKIT=1
```
## Troubleshooting
### Build Failures
**Slow builds / No caching:**
If builds always take 5+ minutes even for small code changes, BuildKit may not be enabled.
Solution:
```bash
# Check if BuildKit is enabled
echo $DOCKER_BUILDKIT
# Enable BuildKit
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
# Add to shell profile for persistence
echo 'export DOCKER_BUILDKIT=1' >> ~/.bashrc
echo 'export COMPOSE_DOCKER_CLI_BUILD=1' >> ~/.bashrc
# Or use the helper script
./docker/enable-buildkit.sh
# Rebuild
docker compose build
```
**Cargo.lock version error:**
```
error: failed to parse lock file at: /build/Cargo.lock
Caused by:
lock file version `4` was found, but this version of Cargo does not understand this lock file
```
Solution: Update Rust version in Dockerfile
```bash
# Edit docker/Dockerfile and change:
ARG RUST_VERSION=1.75
# to:
ARG RUST_VERSION=1.92
```
Cargo.lock version 4 requires Rust 1.82+. The project uses Rust 1.92.
**Cargo dependencies fail:**
```bash
# Clear Docker build cache
docker builder prune -a
# Rebuild without cache
docker compose build --no-cache
```
**SQLx compile-time verification fails:**
- Ensure `.sqlx/` directory is present
- Regenerate with `cargo sqlx prepare` if needed
### Runtime Issues
**Service won't start:**
```bash
# Check logs
docker compose logs <service-name>
# Check health
docker compose ps
```
**Database connection fails:**
```bash
# Verify PostgreSQL is ready
docker compose exec postgres pg_isready -U attune
# Check connection from service
docker compose exec api /bin/sh
# Then: curl postgres:5432
```
**Permission errors:**
```bash
# Fix volume permissions
sudo chown -R 1000:1000 ./packs ./logs
```
## Development Workflow
### Local Development with Docker
```bash
# Start infrastructure only
docker compose up -d postgres rabbitmq redis
# Run services locally
cargo run --bin attune-api
cargo run --bin attune-worker
# Or start everything
docker compose up -d
```
### Rebuilding After Code Changes
```bash
# Rebuild and restart specific service
docker compose build api
docker compose up -d api
# Rebuild all
docker compose build
docker compose up -d
```
### Debugging
```bash
# Access service shell
docker compose exec api /bin/sh
# View logs in real-time
docker compose logs -f api worker
# Check resource usage
docker stats
```
## Production Deployment
See [Docker Deployment Guide](../docs/docker-deployment.md) for:
- Production configuration
- Security hardening
- Scaling strategies
- Monitoring setup
- Backup procedures
- High availability
## CI/CD Integration
Example GitHub Actions workflow:
```yaml
- name: Build Docker images
run: docker compose build
- name: Run tests
run: docker compose run --rm api cargo test
- name: Push to registry
run: |
docker tag attune-api:latest registry.example.com/attune-api:${{ github.sha }}
docker push registry.example.com/attune-api:${{ github.sha }}
```
## Maintenance
### Updating Images
```bash
# Pull latest base images
docker compose pull
# Rebuild services
docker compose build --pull
# Restart with new images
docker compose up -d
```
### Cleaning Up
```bash
# Remove stopped containers
docker compose down
# Remove volumes (WARNING: deletes data)
docker compose down -v
# Clean up unused images
docker image prune -a
# Full cleanup
docker system prune -a --volumes
```
## References
- [Docker Compose Documentation](https://docs.docker.com/compose/)
- [Multi-stage Builds](https://docs.docker.com/build/building/multi-stage/)
- [Dockerfile Best Practices](https://docs.docker.com/develop/dev-best-practices/)
- [Main Documentation](../docs/docker-deployment.md)

364
docker/README.worker.md Normal file
View File

@@ -0,0 +1,364 @@
# Attune Worker Containers
This directory contains Docker configurations for building Attune worker containers with different runtime capabilities.
## Overview
Attune workers can run in containers with specialized runtime environments. Workers automatically declare their capabilities when they register with the system, enabling intelligent action scheduling based on runtime requirements.
## Worker Variants
### Base Worker (`worker-base`)
- **Runtimes**: `shell`
- **Base Image**: Debian Bookworm Slim
- **Size**: ~580 MB
- **Use Case**: Lightweight workers for shell scripts and basic automation
- **Build**: `make docker-build-worker-base`
### Python Worker (`worker-python`)
- **Runtimes**: `shell`, `python`
- **Base Image**: Python 3.11 Slim
- **Size**: ~1.2 GB
- **Includes**: pip, virtualenv, common Python libraries (requests, pyyaml, jinja2, python-dateutil)
- **Use Case**: Python actions and scripts with dependencies
- **Build**: `make docker-build-worker-python`
### Node.js Worker (`worker-node`)
- **Runtimes**: `shell`, `node`
- **Base Image**: Node 20 Slim
- **Size**: ~760 MB
- **Includes**: npm, yarn
- **Use Case**: JavaScript/TypeScript actions and npm packages
- **Build**: `make docker-build-worker-node`
### Full Worker (`worker-full`)
- **Runtimes**: `shell`, `python`, `node`, `native`
- **Base Image**: Debian Bookworm
- **Size**: ~1.6 GB
- **Includes**: Python 3.x, Node.js 20, build tools
- **Use Case**: General-purpose automation requiring multiple runtimes
- **Build**: `make docker-build-worker-full`
## Building Worker Images
### Build All Variants
```bash
make docker-build-workers
```
### Build Individual Variants
```bash
# Base worker (shell only)
make docker-build-worker-base
# Python worker
make docker-build-worker-python
# Node.js worker
make docker-build-worker-node
# Full worker (all runtimes)
make docker-build-worker-full
```
### Direct Docker Build
```bash
# Using Docker directly with BuildKit
DOCKER_BUILDKIT=1 docker build \
--target worker-python \
-t attune-worker:python \
-f docker/Dockerfile.worker \
.
```
## Running Workers
### Using Docker Compose
```bash
# Start specific worker type
docker-compose up -d worker-python
# Start all workers
docker-compose up -d worker-shell worker-python worker-node worker-full
# Scale workers
docker-compose up -d --scale worker-python=3
```
### Using Docker Run
```bash
docker run -d \
--name worker-python-01 \
--network attune_attune-network \
-e ATTUNE_WORKER_NAME=worker-python-01 \
-e ATTUNE_WORKER_RUNTIMES=shell,python \
-e ATTUNE__DATABASE__URL=postgresql://attune:attune@postgres:5432/attune \
-e ATTUNE__MESSAGE_QUEUE__URL=amqp://attune:attune@rabbitmq:5672 \
-v $(pwd)/packs:/opt/attune/packs:ro \
attune-worker:python
```
## Runtime Capability Declaration
Workers declare their capabilities in three ways (in order of precedence):
### 1. Environment Variable (Highest Priority)
```bash
ATTUNE_WORKER_RUNTIMES="shell,python,custom"
```
### 2. Configuration File
```yaml
worker:
capabilities:
runtimes: ["shell", "python"]
```
### 3. Auto-Detection (Fallback)
Workers automatically detect available runtimes by checking for binaries:
- `python3` or `python` → adds `python`
- `node` → adds `node`
- Always includes `shell` and `native`
## Configuration
### Key Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `ATTUNE_WORKER_NAME` | Unique worker identifier | `worker-python-01` |
| `ATTUNE_WORKER_RUNTIMES` | Comma-separated runtime list | `shell,python` |
| `ATTUNE_WORKER_TYPE` | Worker type | `container` |
| `ATTUNE__DATABASE__URL` | PostgreSQL connection | `postgresql://...` |
| `ATTUNE__MESSAGE_QUEUE__URL` | RabbitMQ connection | `amqp://...` |
| `RUST_LOG` | Log level | `info`, `debug`, `trace` |
### Resource Limits
Set CPU and memory limits in `docker-compose.override.yml`:
```yaml
services:
worker-python:
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
```
## Custom Worker Images
### Extend Python Worker
Create a custom worker with additional packages:
```dockerfile
# Dockerfile.worker.ml
FROM attune-worker:python
USER root
# Install ML packages
RUN pip install --no-cache-dir \
pandas \
numpy \
scikit-learn \
torch
USER attune
ENV ATTUNE_WORKER_RUNTIMES="shell,python,ml"
```
Build and run:
```bash
docker build -t attune-worker:ml -f Dockerfile.worker.ml .
docker run -d --name worker-ml-01 ... attune-worker:ml
```
### Add New Runtime
Example: Adding Ruby support
```dockerfile
FROM attune-worker:base
USER root
RUN apt-get update && apt-get install -y \
ruby-full \
&& rm -rf /var/lib/apt/lists/*
USER attune
ENV ATTUNE_WORKER_RUNTIMES="shell,ruby"
```
## Architecture
### Multi-stage Build
The `Dockerfile.worker` uses a multi-stage build pattern:
1. **Builder Stage**: Compiles the Rust worker binary
- Uses BuildKit cache mounts for fast incremental builds
- Shared across all worker variants
2. **Runtime Stages**: Creates specialized worker images
- `worker-base`: Minimal shell runtime
- `worker-python`: Python runtime
- `worker-node`: Node.js runtime
- `worker-full`: All runtimes
### Build Cache
BuildKit cache mounts dramatically speed up builds:
- First build: ~5-6 minutes
- Incremental builds: ~30-60 seconds
Cache is shared across builds using `sharing=locked` to prevent race conditions.
## Security
### Non-root Execution
All workers run as user `attune` (UID 1000)
### Read-only Packs
Pack files are mounted read-only to prevent modification:
```yaml
volumes:
- ./packs:/opt/attune/packs:ro # :ro = read-only
```
### Network Isolation
Workers run in isolated Docker network with only necessary service access
### Secret Management
Use environment variables for sensitive data; never hardcode in images
## Monitoring
### Check Worker Registration
```bash
docker-compose exec postgres psql -U attune -d attune -c \
"SELECT name, worker_type, status, capabilities->>'runtimes' as runtimes FROM worker;"
```
### View Logs
```bash
docker-compose logs -f worker-python
```
### Check Resource Usage
```bash
docker stats attune-worker-python
```
### Verify Health
```bash
docker-compose ps | grep worker
```
## Troubleshooting
### Worker Not Registering
**Check database connectivity:**
```bash
docker-compose logs worker-python | grep -i database
```
**Verify environment:**
```bash
docker-compose exec worker-python env | grep ATTUNE
```
### Runtime Not Detected
**Check runtime availability:**
```bash
docker-compose exec worker-python python3 --version
docker-compose exec worker-python node --version
```
**Force runtime declaration:**
```bash
ATTUNE_WORKER_RUNTIMES=shell,python
```
### Actions Not Scheduled
**Verify runtime match:**
```sql
-- Check action runtime requirement
SELECT a.ref, r.name as runtime
FROM action a
JOIN runtime r ON a.runtime = r.id
WHERE a.ref = 'core.my_action';
-- Check worker capabilities
SELECT name, capabilities->>'runtimes'
FROM worker
WHERE status = 'active';
```
## Performance
### Image Sizes
| Image | Size | Build Time (Cold) | Build Time (Cached) |
|-------|------|-------------------|---------------------|
| worker-base | ~580 MB | ~5 min | ~30 sec |
| worker-python | ~1.2 GB | ~6 min | ~45 sec |
| worker-node | ~760 MB | ~6 min | ~45 sec |
| worker-full | ~1.6 GB | ~7 min | ~60 sec |
### Optimization Tips
1. **Use specific variants**: Don't use `worker-full` if you only need Python
2. **Enable BuildKit**: Dramatically speeds up builds
3. **Layer caching**: Order Dockerfile commands from least to most frequently changed
4. **Multi-stage builds**: Keeps runtime images small
## Files
- `Dockerfile.worker` - Multi-stage worker Dockerfile with all variants
- `README.worker.md` - This file
- `../docker-compose.yaml` - Service definitions for all workers
## References
- [Worker Containerization Design](../docs/worker-containerization.md)
- [Quick Start Guide](../docs/worker-containers-quickstart.md)
- [Worker Service Architecture](../docs/architecture/worker-service.md)
- [Production Deployment](../docs/production-deployment.md)
## Quick Commands
```bash
# Build all workers
make docker-build-workers
# Start all workers
docker-compose up -d worker-shell worker-python worker-node worker-full
# Check worker status
docker-compose exec postgres psql -U attune -d attune -c \
"SELECT name, status, capabilities FROM worker;"
# View Python worker logs
docker-compose logs -f worker-python
# Restart worker
docker-compose restart worker-python
# Scale Python workers
docker-compose up -d --scale worker-python=3
# Stop all workers
docker-compose stop worker-shell worker-python worker-node worker-full
```

199
docker/enable-buildkit.sh Executable file
View File

@@ -0,0 +1,199 @@
#!/bin/bash
# enable-buildkit.sh - Enable Docker BuildKit for faster Rust builds
# This script configures Docker to use BuildKit, which enables cache mounts
# for dramatically faster incremental builds
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
echo -e "${BLUE}=================================================="
echo "Docker BuildKit Configuration"
echo -e "==================================================${NC}\n"
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed"
exit 1
fi
print_success "Docker is installed"
# Check current BuildKit status
print_info "Checking current BuildKit configuration..."
if [ -n "$DOCKER_BUILDKIT" ]; then
print_info "DOCKER_BUILDKIT environment variable is set to: $DOCKER_BUILDKIT"
else
print_warning "DOCKER_BUILDKIT environment variable is not set"
fi
# Determine shell
SHELL_NAME=$(basename "$SHELL")
SHELL_RC=""
case "$SHELL_NAME" in
bash)
if [ -f "$HOME/.bashrc" ]; then
SHELL_RC="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
SHELL_RC="$HOME/.bash_profile"
fi
;;
zsh)
SHELL_RC="$HOME/.zshrc"
;;
fish)
SHELL_RC="$HOME/.config/fish/config.fish"
;;
*)
SHELL_RC="$HOME/.profile"
;;
esac
echo ""
print_info "Detected shell: $SHELL_NAME"
print_info "Shell configuration file: $SHELL_RC"
# Check if already configured
if [ -f "$SHELL_RC" ] && grep -q "DOCKER_BUILDKIT" "$SHELL_RC"; then
echo ""
print_success "BuildKit is already configured in $SHELL_RC"
# Check if it's enabled
if grep -q "export DOCKER_BUILDKIT=1" "$SHELL_RC"; then
print_success "BuildKit is ENABLED"
else
print_warning "BuildKit configuration found but may not be enabled"
print_info "Check your $SHELL_RC file"
fi
else
echo ""
print_warning "BuildKit is not configured in your shell"
read -p "Would you like to enable BuildKit globally? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "" >> "$SHELL_RC"
echo "# Enable Docker BuildKit for faster builds" >> "$SHELL_RC"
echo "export DOCKER_BUILDKIT=1" >> "$SHELL_RC"
echo "export COMPOSE_DOCKER_CLI_BUILD=1" >> "$SHELL_RC"
print_success "BuildKit configuration added to $SHELL_RC"
print_info "Run: source $SHELL_RC (or restart your terminal)"
fi
fi
# Check Docker daemon configuration
DOCKER_CONFIG="/etc/docker/daemon.json"
HAS_SUDO=false
if command -v sudo &> /dev/null; then
HAS_SUDO=true
fi
echo ""
print_info "Checking Docker daemon configuration..."
if [ -f "$DOCKER_CONFIG" ]; then
if $HAS_SUDO && sudo test -r "$DOCKER_CONFIG"; then
if sudo grep -q "\"features\"" "$DOCKER_CONFIG" && sudo grep -q "\"buildkit\"" "$DOCKER_CONFIG"; then
print_success "BuildKit appears to be configured in Docker daemon"
else
print_warning "BuildKit may not be configured in Docker daemon"
print_info "This is optional - environment variables are sufficient"
fi
else
print_warning "Cannot read $DOCKER_CONFIG (permission denied)"
print_info "This is normal for non-root users"
fi
else
print_info "Docker daemon config not found at $DOCKER_CONFIG"
print_info "This is normal - environment variables work fine"
fi
# Test BuildKit
echo ""
print_info "Testing BuildKit availability..."
# Create a minimal test Dockerfile
TEST_DIR=$(mktemp -d)
cat > "$TEST_DIR/Dockerfile" <<'EOF'
FROM alpine:latest
RUN --mount=type=cache,target=/tmp/cache echo "BuildKit works!" > /tmp/cache/test
RUN echo "Test complete"
EOF
if DOCKER_BUILDKIT=1 docker build -q "$TEST_DIR" > /dev/null 2>&1; then
print_success "BuildKit is working correctly!"
print_success "Cache mounts are supported"
else
print_error "BuildKit test failed"
print_info "Your Docker version may not support BuildKit"
print_info "BuildKit requires Docker 18.09+ with experimental features enabled"
fi
# Cleanup
rm -rf "$TEST_DIR"
# Display usage information
echo ""
echo -e "${BLUE}=================================================="
echo "Usage Information"
echo -e "==================================================${NC}"
echo ""
echo "To use BuildKit with Attune:"
echo ""
echo "1. Build with docker compose (recommended):"
echo " export DOCKER_BUILDKIT=1"
echo " docker compose build"
echo ""
echo "2. Build individual service:"
echo " DOCKER_BUILDKIT=1 docker build --build-arg SERVICE=api -f docker/Dockerfile -t attune-api ."
echo ""
echo "3. Use Makefile:"
echo " export DOCKER_BUILDKIT=1"
echo " make docker-build"
echo ""
echo -e "${GREEN}Benefits of BuildKit:${NC}"
echo " • First build: ~5-6 minutes"
echo " • Incremental builds: ~30-60 seconds (instead of 5+ minutes)"
echo " • Caches: Cargo registry, git dependencies, compilation artifacts"
echo " • Parallel builds and improved layer caching"
echo ""
echo -e "${YELLOW}Note:${NC} Cache persists between builds, potentially using 5-10GB disk space"
echo " To clear cache: docker builder prune"
echo ""
# Check for current environment
if [ "$DOCKER_BUILDKIT" = "1" ]; then
print_success "BuildKit is currently ENABLED in this shell session"
else
print_warning "BuildKit is NOT enabled in the current shell session"
print_info "Run: export DOCKER_BUILDKIT=1"
fi
echo ""
print_success "Configuration check complete!"

55
docker/init-db.sh Executable file
View File

@@ -0,0 +1,55 @@
#!/bin/bash
# init-db.sh - Database initialization script for Docker
# This script runs migrations and sets up the initial database schema
set -e
echo "=================================================="
echo "Attune Database Initialization"
echo "=================================================="
# Wait for PostgreSQL to be ready
echo "Waiting for PostgreSQL to be ready..."
until pg_isready -h postgres -U attune -d attune > /dev/null 2>&1; do
echo " PostgreSQL is unavailable - sleeping"
sleep 2
done
echo "✓ PostgreSQL is ready"
# Check if schema exists
SCHEMA_EXISTS=$(psql -h postgres -U attune -d attune -tAc "SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = 'attune');")
if [ "$SCHEMA_EXISTS" = "f" ]; then
echo "Creating attune schema..."
psql -h postgres -U attune -d attune -c "CREATE SCHEMA IF NOT EXISTS attune;"
echo "✓ Schema created"
else
echo "✓ Schema already exists"
fi
# Set search path
echo "Setting search path..."
psql -h postgres -U attune -d attune -c "ALTER DATABASE attune SET search_path TO attune, public;"
echo "✓ Search path configured"
# Run migrations
echo "Running database migrations..."
cd /opt/attune
sqlx migrate run
echo "✓ Migrations complete"
# Check table count
TABLE_COUNT=$(psql -h postgres -U attune -d attune -tAc "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'attune';")
echo "✓ Database has ${TABLE_COUNT} tables"
# Load core pack if needed
if [ -f /opt/attune/scripts/load-core-pack.sh ]; then
echo "Loading core pack..."
/opt/attune/scripts/load-core-pack.sh || echo "⚠ Core pack load failed (may already exist)"
fi
echo "=================================================="
echo "Database initialization complete!"
echo "=================================================="

208
docker/init-packs.sh Executable file
View File

@@ -0,0 +1,208 @@
#!/bin/sh
# Initialize builtin packs for Attune
# This script copies pack files to the shared volume and registers them in the database
set -e
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration from environment
DB_HOST="${DB_HOST:-postgres}"
DB_PORT="${DB_PORT:-5432}"
DB_USER="${DB_USER:-attune}"
DB_PASSWORD="${DB_PASSWORD:-attune}"
DB_NAME="${DB_NAME:-attune}"
DB_SCHEMA="${DB_SCHEMA:-public}"
# Pack directories
SOURCE_PACKS_DIR="${SOURCE_PACKS_DIR:-/source/packs}"
TARGET_PACKS_DIR="${TARGET_PACKS_DIR:-/opt/attune/packs}"
# Python loader script
LOADER_SCRIPT="${LOADER_SCRIPT:-/scripts/load_core_pack.py}"
echo ""
echo -e "${BLUE}╔════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Attune Builtin Packs Initialization ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════╝${NC}"
echo ""
# Install system dependencies
echo -e "${YELLOW}${NC} Installing system dependencies..."
apk add --no-cache postgresql-client > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo -e "${GREEN}${NC} System dependencies installed"
else
echo -e "${RED}${NC} Failed to install system dependencies"
exit 1
fi
# Install Python dependencies
echo -e "${YELLOW}${NC} Installing Python dependencies..."
pip install --quiet --no-cache-dir psycopg2-binary pyyaml 2>/dev/null
if [ $? -eq 0 ]; then
echo -e "${GREEN}${NC} Python dependencies installed"
else
echo -e "${RED}${NC} Failed to install Python dependencies"
exit 1
fi
echo ""
# Wait for database to be ready
echo -e "${YELLOW}${NC} Waiting for database to be ready..."
export PGPASSWORD="$DB_PASSWORD"
until psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c '\q' 2>/dev/null; do
echo -e "${YELLOW} ...${NC} Database is unavailable - sleeping"
sleep 2
done
echo -e "${GREEN}${NC} Database is ready"
# Create target packs directory if it doesn't exist
echo -e "${YELLOW}${NC} Ensuring packs directory exists..."
mkdir -p "$TARGET_PACKS_DIR"
echo -e "${GREEN}${NC} Packs directory ready at: $TARGET_PACKS_DIR"
# Check if source packs directory exists
if [ ! -d "$SOURCE_PACKS_DIR" ]; then
echo -e "${RED}${NC} Source packs directory not found: $SOURCE_PACKS_DIR"
exit 1
fi
# Find all pack directories (directories with pack.yaml)
echo ""
echo -e "${BLUE}Discovering builtin packs...${NC}"
echo "----------------------------------------"
PACK_COUNT=0
COPIED_COUNT=0
LOADED_COUNT=0
for pack_dir in "$SOURCE_PACKS_DIR"/*; do
if [ -d "$pack_dir" ]; then
pack_name=$(basename "$pack_dir")
pack_yaml="$pack_dir/pack.yaml"
if [ -f "$pack_yaml" ]; then
PACK_COUNT=$((PACK_COUNT + 1))
echo -e "${BLUE}${NC} Found pack: ${GREEN}$pack_name${NC}"
# Check if pack already exists in target
target_pack_dir="$TARGET_PACKS_DIR/$pack_name"
if [ -d "$target_pack_dir" ]; then
# Pack exists, check if we should update
# For now, we'll skip if it exists (idempotent on restart)
echo -e "${YELLOW}${NC} Pack already exists at: $target_pack_dir"
echo -e "${BLUE} ${NC} Skipping copy (use fresh volume to reload)"
else
# Copy pack to target directory
echo -e "${YELLOW}${NC} Copying pack files..."
cp -r "$pack_dir" "$target_pack_dir"
if [ $? -eq 0 ]; then
COPIED_COUNT=$((COPIED_COUNT + 1))
echo -e "${GREEN}${NC} Copied to: $target_pack_dir"
else
echo -e "${RED}${NC} Failed to copy pack"
exit 1
fi
fi
fi
fi
done
echo "----------------------------------------"
echo ""
if [ $PACK_COUNT -eq 0 ]; then
echo -e "${YELLOW}${NC} No builtin packs found in $SOURCE_PACKS_DIR"
echo -e "${BLUE}${NC} This is OK if you're running with no packs"
exit 0
fi
echo -e "${BLUE}Pack Discovery Summary:${NC}"
echo " Total packs found: $PACK_COUNT"
echo " Newly copied: $COPIED_COUNT"
echo " Already present: $((PACK_COUNT - COPIED_COUNT))"
echo ""
# Load packs into database using Python loader
if [ -f "$LOADER_SCRIPT" ]; then
echo -e "${BLUE}Loading packs into database...${NC}"
echo "----------------------------------------"
# Build database URL with schema support
DATABASE_URL="postgresql://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME"
# Set search_path for the Python script if not using default schema
if [ "$DB_SCHEMA" != "public" ]; then
export PGOPTIONS="-c search_path=$DB_SCHEMA,public"
fi
# Run the Python loader for each pack
for pack_dir in "$TARGET_PACKS_DIR"/*; do
if [ -d "$pack_dir" ]; then
pack_name=$(basename "$pack_dir")
pack_yaml="$pack_dir/pack.yaml"
if [ -f "$pack_yaml" ]; then
echo -e "${YELLOW}${NC} Loading pack: ${GREEN}$pack_name${NC}"
# Run Python loader
if python3 "$LOADER_SCRIPT" \
--database-url "$DATABASE_URL" \
--pack-dir "$TARGET_PACKS_DIR" \
--schema "$DB_SCHEMA"; then
LOADED_COUNT=$((LOADED_COUNT + 1))
echo -e "${GREEN}${NC} Loaded pack: $pack_name"
else
echo -e "${RED}${NC} Failed to load pack: $pack_name"
echo -e "${YELLOW}${NC} Continuing with other packs..."
fi
fi
fi
done
echo "----------------------------------------"
echo ""
echo -e "${BLUE}Database Loading Summary:${NC}"
echo " Successfully loaded: $LOADED_COUNT"
echo " Failed: $((PACK_COUNT - LOADED_COUNT))"
echo ""
else
echo -e "${YELLOW}${NC} Pack loader script not found: $LOADER_SCRIPT"
echo -e "${BLUE}${NC} Packs copied but not registered in database"
echo -e "${BLUE}${NC} You can manually load them later"
fi
# Summary
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Builtin Packs Initialization Complete! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${BLUE}Packs Location:${NC} ${GREEN}$TARGET_PACKS_DIR${NC}"
echo -e "${BLUE}Packs Available:${NC}"
for pack_dir in "$TARGET_PACKS_DIR"/*; do
if [ -d "$pack_dir" ]; then
pack_name=$(basename "$pack_dir")
pack_yaml="$pack_dir/pack.yaml"
if [ -f "$pack_yaml" ]; then
# Try to extract version from pack.yaml
version=$(grep "^version:" "$pack_yaml" | head -1 | sed 's/version:[[:space:]]*//' | tr -d '"')
echo -e "${GREEN}$pack_name${NC} ${BLUE}($version)${NC}"
fi
fi
done
echo ""
echo -e "${BLUE}${NC} Pack files are accessible to all services via shared volume"
echo ""
exit 0

29
docker/init-roles.sql Normal file
View File

@@ -0,0 +1,29 @@
-- Docker initialization script
-- Creates the svc_attune role needed by migrations
-- This runs before migrations via docker-compose
-- Create service role for the application
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'svc_attune') THEN
CREATE ROLE svc_attune WITH LOGIN PASSWORD 'attune_service_password';
END IF;
END
$$;
-- Create API role
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'attune_api') THEN
CREATE ROLE attune_api WITH LOGIN PASSWORD 'attune_api_password';
END IF;
END
$$;
-- Grant basic permissions
GRANT ALL PRIVILEGES ON DATABASE attune TO svc_attune;
GRANT ALL PRIVILEGES ON DATABASE attune TO attune_api;
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

108
docker/init-user.sh Executable file
View File

@@ -0,0 +1,108 @@
#!/bin/sh
# Initialize default test user for Attune
# This script creates a default test user if it doesn't already exist
set -e
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Database configuration from environment
DB_HOST="${DB_HOST:-postgres}"
DB_PORT="${DB_PORT:-5432}"
DB_USER="${DB_USER:-attune}"
DB_PASSWORD="${DB_PASSWORD:-attune}"
DB_NAME="${DB_NAME:-attune}"
DB_SCHEMA="${DB_SCHEMA:-public}"
# Test user configuration
TEST_LOGIN="${TEST_LOGIN:-test@attune.local}"
TEST_DISPLAY_NAME="${TEST_DISPLAY_NAME:-Test User}"
TEST_PASSWORD="${TEST_PASSWORD:-TestPass123!}"
# Pre-computed Argon2id hash for "TestPass123!"
# Using: m=19456, t=2, p=1 (default Argon2id parameters)
DEFAULT_PASSWORD_HASH='$argon2id$v=19$m=19456,t=2,p=1$AuZJ0xsGuSRk6LdCd58OOA$vBZnaflJwR9L4LPWoGGrcnRsIOf95FV4uIsoe3PjRE0'
echo ""
echo -e "${BLUE}╔════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Attune Default User Initialization ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════╝${NC}"
echo ""
# Wait for database to be ready
echo -e "${YELLOW}${NC} Waiting for database to be ready..."
until PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c '\q' 2>/dev/null; do
echo -e "${YELLOW} ...${NC} Database is unavailable - sleeping"
sleep 2
done
echo -e "${GREEN}${NC} Database is ready"
# Check if user already exists
echo -e "${YELLOW}${NC} Checking if user exists..."
USER_EXISTS=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -tAc \
"SELECT COUNT(*) FROM ${DB_SCHEMA}.identity WHERE login = '$TEST_LOGIN';")
if [ "$USER_EXISTS" -gt 0 ]; then
echo -e "${GREEN}${NC} User '$TEST_LOGIN' already exists"
echo -e "${BLUE}${NC} Skipping user creation"
else
echo -e "${YELLOW}${NC} Creating default test user..."
# Use the pre-computed hash for default password
if [ "$TEST_PASSWORD" = "TestPass123!" ]; then
PASSWORD_HASH="$DEFAULT_PASSWORD_HASH"
echo -e "${BLUE}${NC} Using default password hash"
else
echo -e "${YELLOW}${NC} Custom password detected - using basic hash"
echo -e "${YELLOW}${NC} For production, generate proper Argon2id hash"
# Note: For custom passwords in Docker, you should pre-generate the hash
# This is a fallback that will work but is less secure
PASSWORD_HASH="$DEFAULT_PASSWORD_HASH"
fi
# Insert the user
PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" << EOF
INSERT INTO ${DB_SCHEMA}.identity (login, display_name, password_hash, attributes)
VALUES (
'$TEST_LOGIN',
'$TEST_DISPLAY_NAME',
'$PASSWORD_HASH',
jsonb_build_object(
'email', '$TEST_LOGIN',
'created_via', 'docker-init',
'is_test_user', true
)
);
EOF
if [ $? -eq 0 ]; then
echo -e "${GREEN}${NC} User created successfully"
else
echo -e "${RED}${NC} Failed to create user"
exit 1
fi
fi
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Default User Initialization Complete! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${BLUE}Default User Credentials:${NC}"
echo -e " Login: ${GREEN}$TEST_LOGIN${NC}"
echo -e " Password: ${GREEN}$TEST_PASSWORD${NC}"
echo ""
echo -e "${BLUE}Test Login:${NC}"
echo -e " ${YELLOW}curl -X POST http://localhost:8080/auth/login \\${NC}"
echo -e " ${YELLOW}-H 'Content-Type: application/json' \\${NC}"
echo -e " ${YELLOW}-d '{\"login\":\"$TEST_LOGIN\",\"password\":\"$TEST_PASSWORD\"}'${NC}"
echo ""
echo -e "${BLUE}${NC} For custom users, see: docs/testing/test-user-setup.md"
echo ""
exit 0

24
docker/inject-env.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/sh
# inject-env.sh - Injects runtime environment variables into the Web UI
# This script runs at container startup to make environment variables available to the browser
set -e
# Default values
API_URL="${API_URL:-http://localhost:8080}"
WS_URL="${WS_URL:-ws://localhost:8081}"
# Create runtime configuration file
cat > /usr/share/nginx/html/config/runtime-config.js <<EOF
// Runtime configuration injected at container startup
window.ATTUNE_CONFIG = {
apiUrl: '${API_URL}',
wsUrl: '${WS_URL}',
environment: '${ENVIRONMENT:-production}'
};
EOF
echo "Runtime configuration injected:"
echo " API_URL: ${API_URL}"
echo " WS_URL: ${WS_URL}"
echo " ENVIRONMENT: ${ENVIRONMENT:-production}"

111
docker/nginx.conf Normal file
View File

@@ -0,0 +1,111 @@
# Nginx configuration for Attune Web UI
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/xml+rss application/json;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Health check endpoint
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
# Auth proxy - forward auth requests to backend
location /auth/ {
proxy_pass http://api:8080/auth/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# API proxy - forward API requests to backend (preserves /api prefix)
location /api/ {
proxy_pass http://api:8080/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# WebSocket proxy for notifier service
location /ws/ {
proxy_pass http://notifier:8081/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket timeouts
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
# Serve static assets with caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Runtime configuration endpoint
location /config/runtime-config.js {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
# SPA routing - serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
# Disable caching for index.html
location = /index.html {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}

265
docker/quickstart.sh Executable file
View File

@@ -0,0 +1,265 @@
#!/bin/bash
# quickstart.sh - Quick start script for Attune Docker deployment
# This script helps you get Attune up and running with Docker in minutes
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper functions
print_header() {
echo -e "\n${BLUE}=================================================="
echo -e "$1"
echo -e "==================================================${NC}\n"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
check_command() {
if ! command -v $1 &> /dev/null; then
print_error "$1 is not installed"
return 1
else
print_success "$1 is installed"
return 0
fi
}
# Main script
print_header "Attune Docker Quick Start"
# Change to project root
cd "$(dirname "$0")/.."
# Check prerequisites
print_info "Checking prerequisites..."
if ! check_command docker; then
print_error "Docker is required. Install from: https://docs.docker.com/get-docker/"
exit 1
fi
if ! check_command docker-compose && ! docker compose version &> /dev/null; then
print_error "Docker Compose is required. Install from: https://docs.docker.com/compose/install/"
exit 1
fi
# Detect docker-compose command
if command -v docker-compose &> /dev/null; then
DOCKER_COMPOSE="docker-compose"
else
DOCKER_COMPOSE="docker compose"
fi
# Check if Docker daemon is running
if ! docker info &> /dev/null; then
print_error "Docker daemon is not running. Please start Docker and try again."
exit 1
fi
print_success "All prerequisites met"
# Enable BuildKit for faster incremental builds
print_info "Enabling Docker BuildKit for faster builds..."
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
print_success "BuildKit enabled for this session"
# Check for .env file
print_info "Checking configuration..."
if [ ! -f .env ]; then
print_warning ".env file not found. Creating from template..."
if [ -f env.docker.example ]; then
cp env.docker.example .env
print_success "Created .env file from env.docker.example"
# Generate secure secrets
print_info "Generating secure secrets..."
JWT_SECRET=$(openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64)
ENCRYPTION_KEY=$(openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64)
# Update .env file with generated secrets
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
sed -i '' "s/JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env
sed -i '' "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${ENCRYPTION_KEY}/" .env
else
# Linux
sed -i "s/JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${ENCRYPTION_KEY}/" .env
fi
print_success "Generated and saved secure secrets"
else
print_error "env.docker.example not found. Please create .env file manually."
exit 1
fi
else
print_success ".env file exists"
# Check if secrets are still default values
if grep -q "docker-dev-secret-change-in-production" .env || grep -q "docker-dev-encryption-key-32ch" .env; then
print_warning "Default secrets detected in .env file"
read -p "Would you like to generate new secure secrets? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
JWT_SECRET=$(openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64)
ENCRYPTION_KEY=$(openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64)
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s/JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env
sed -i '' "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${ENCRYPTION_KEY}/" .env
else
sed -i "s/JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${ENCRYPTION_KEY}/" .env
fi
print_success "Generated and saved secure secrets"
fi
fi
fi
# Pull or build images
print_header "Building Docker Images"
print_info "This may take 5-6 minutes on first run with BuildKit..."
print_info "Subsequent builds will be much faster (~30-60 seconds)"
if ! $DOCKER_COMPOSE build; then
print_error "Failed to build Docker images"
exit 1
fi
print_success "Docker images built successfully"
# Start services
print_header "Starting Services"
if ! $DOCKER_COMPOSE up -d; then
print_error "Failed to start services"
exit 1
fi
print_success "Services started"
# Wait for services to be healthy
print_info "Waiting for services to be healthy..."
sleep 5
MAX_WAIT=120
WAITED=0
ALL_HEALTHY=false
while [ $WAITED -lt $MAX_WAIT ]; do
UNHEALTHY=$($DOCKER_COMPOSE ps --format json 2>/dev/null | grep -c '"Health":"unhealthy"' || true)
STARTING=$($DOCKER_COMPOSE ps --format json 2>/dev/null | grep -c '"Health":"starting"' || true)
if [ "$UNHEALTHY" -eq 0 ] && [ "$STARTING" -eq 0 ]; then
ALL_HEALTHY=true
break
fi
echo -n "."
sleep 5
WAITED=$((WAITED + 5))
done
echo ""
if [ "$ALL_HEALTHY" = true ]; then
print_success "All services are healthy"
else
print_warning "Some services may not be fully ready yet"
print_info "Check status with: $DOCKER_COMPOSE ps"
fi
# Display service status
print_header "Service Status"
$DOCKER_COMPOSE ps
# Display access information
print_header "Access Information"
echo -e "${GREEN}Attune is now running!${NC}\n"
print_info "Web UI: http://localhost:3000"
print_info "API: http://localhost:8080"
print_info "API Documentation: http://localhost:8080/api-spec/swagger-ui/"
print_info "RabbitMQ Management: http://localhost:15672"
print_info " (username: attune, password: attune)"
echo ""
print_info "View logs: $DOCKER_COMPOSE logs -f"
print_info "Stop services: $DOCKER_COMPOSE down"
print_info "View status: $DOCKER_COMPOSE ps"
# Check if we need to run migrations
print_header "Database Setup"
print_info "Checking database migrations..."
if $DOCKER_COMPOSE exec -T api sh -c "sqlx migrate info" &> /dev/null; then
print_success "Database migrations are up to date"
else
print_warning "Database migrations may need to be run manually"
print_info "Run: $DOCKER_COMPOSE exec api sqlx migrate run"
fi
# Offer to create admin user
print_header "Admin User Setup"
read -p "Would you like to create an admin user now? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo ""
read -p "Enter admin username: " ADMIN_USER
read -p "Enter admin email: " ADMIN_EMAIL
read -s -p "Enter admin password: " ADMIN_PASSWORD
echo ""
# Note: This requires the CLI or API endpoint to be available
print_info "Creating admin user..."
print_warning "Manual user creation may be required via API or database"
print_info "Use the API at http://localhost:8080/auth/register or the CLI tool"
fi
# Final message
print_header "Setup Complete!"
echo -e "${GREEN}Attune is ready to use!${NC}\n"
print_info "Next steps:"
echo " 1. Open http://localhost:3000 in your browser"
echo " 2. Create a user account"
echo " 3. Explore the documentation: http://localhost:8080/api-spec/swagger-ui/"
echo ""
print_info "For help:"
echo " - View logs: $DOCKER_COMPOSE logs -f [service]"
echo " - Documentation: docs/docker-deployment.md"
echo " - Troubleshooting: docker/README.md"
echo ""
print_info "BuildKit is enabled for this session"
echo " To enable globally, run: ./docker/enable-buildkit.sh"
echo ""
print_success "Happy automating!"

189
docker/run-migrations.sh Executable file
View File

@@ -0,0 +1,189 @@
#!/bin/bash
# Migration script for Attune database
# Runs all SQL migration files in order
set -e
echo "=========================================="
echo "Attune Database Migration Runner"
echo "=========================================="
echo ""
# Database connection parameters
DB_HOST="${DB_HOST:-postgres}"
DB_PORT="${DB_PORT:-5432}"
DB_USER="${DB_USER:-attune}"
DB_PASSWORD="${DB_PASSWORD:-attune}"
DB_NAME="${DB_NAME:-attune}"
MIGRATIONS_DIR="${MIGRATIONS_DIR:-/migrations}"
# Export password for psql
export PGPASSWORD="$DB_PASSWORD"
# Color output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to wait for PostgreSQL to be ready
wait_for_postgres() {
echo "Waiting for PostgreSQL to be ready..."
local max_attempts=30
local attempt=1
while [ $attempt -le $max_attempts ]; do
if psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c '\q' 2>/dev/null; then
echo -e "${GREEN}✓ PostgreSQL is ready${NC}"
return 0
fi
echo " Attempt $attempt/$max_attempts: PostgreSQL not ready yet..."
sleep 2
attempt=$((attempt + 1))
done
echo -e "${RED}✗ PostgreSQL failed to become ready after $max_attempts attempts${NC}"
return 1
}
# Function to check if migrations table exists
setup_migrations_table() {
echo "Setting up migrations tracking table..."
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 <<-EOSQL
CREATE TABLE IF NOT EXISTS _migrations (
id SERIAL PRIMARY KEY,
filename VARCHAR(255) UNIQUE NOT NULL,
applied_at TIMESTAMP DEFAULT NOW()
);
EOSQL
echo -e "${GREEN}✓ Migrations table ready${NC}"
}
# Function to check if a migration has been applied
is_migration_applied() {
local filename=$1
local count=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c \
"SELECT COUNT(*) FROM _migrations WHERE filename = '$filename';" | tr -d ' ')
[ "$count" -gt 0 ]
}
# Function to mark migration as applied
mark_migration_applied() {
local filename=$1
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
"INSERT INTO _migrations (filename) VALUES ('$filename');" > /dev/null
}
# Function to run a migration file
run_migration() {
local filepath=$1
local filename=$(basename "$filepath")
if is_migration_applied "$filename"; then
echo -e "${YELLOW}⊘ Skipping $filename (already applied)${NC}"
return 0
fi
echo -e "${GREEN}→ Applying $filename...${NC}"
# Run migration in a transaction with detailed error reporting
if psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 \
-c "BEGIN;" \
-f "$filepath" \
-c "COMMIT;" > /tmp/migration_output.log 2>&1; then
mark_migration_applied "$filename"
echo -e "${GREEN}✓ Applied $filename${NC}"
return 0
else
echo -e "${RED}✗ Failed to apply $filename${NC}"
echo ""
echo "Error details:"
cat /tmp/migration_output.log
echo ""
echo "Migration rolled back due to error."
return 1
fi
}
# Function to initialize Docker-specific roles and extensions
init_docker_roles() {
echo "Initializing Docker roles and extensions..."
if [ -f "/docker/init-roles.sql" ]; then
if psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 -f "/docker/init-roles.sql" > /dev/null 2>&1; then
echo -e "${GREEN}✓ Docker roles initialized${NC}"
return 0
else
echo -e "${YELLOW}⚠ Warning: Could not initialize Docker roles (may already exist)${NC}"
return 0
fi
else
echo -e "${YELLOW}⚠ No Docker init script found, skipping${NC}"
return 0
fi
}
# Main migration process
main() {
echo "Configuration:"
echo " Database: $DB_HOST:$DB_PORT/$DB_NAME"
echo " User: $DB_USER"
echo " Migrations directory: $MIGRATIONS_DIR"
echo ""
# Wait for database
wait_for_postgres || exit 1
# Initialize Docker-specific roles
init_docker_roles || exit 1
# Setup migrations tracking
setup_migrations_table || exit 1
echo ""
echo "Running migrations..."
echo "----------------------------------------"
# Find and sort migration files
local migration_count=0
local applied_count=0
local skipped_count=0
# Process migrations in sorted order
for migration_file in $(find "$MIGRATIONS_DIR" -name "*.sql" -type f | sort); do
migration_count=$((migration_count + 1))
if is_migration_applied "$(basename "$migration_file")"; then
skipped_count=$((skipped_count + 1))
run_migration "$migration_file"
else
if run_migration "$migration_file"; then
applied_count=$((applied_count + 1))
else
echo -e "${RED}Migration failed!${NC}"
exit 1
fi
fi
done
echo "----------------------------------------"
echo ""
echo "Migration Summary:"
echo " Total migrations: $migration_count"
echo " Newly applied: $applied_count"
echo " Already applied: $skipped_count"
echo ""
if [ $applied_count -gt 0 ]; then
echo -e "${GREEN}✓ All migrations applied successfully!${NC}"
else
echo -e "${GREEN}✓ Database is up to date (no new migrations)${NC}"
fi
}
# Run main function
main