9.7 KiB
Quick Reference: Packs Volume Architecture
TL;DR
Packs are NOT copied into Docker images. They are mounted as volumes.
# Build pack binaries (one-time or when updated)
./scripts/build-pack-binaries.sh
# Start services - init-packs copies packs to volume
docker compose up -d
# Update pack files - no image rebuild needed!
vim packs/core/actions/my_action.yaml
docker compose restart
Architecture Overview
Host Filesystem Docker Volumes Service Containers
───────────────── ─────────────── ──────────────────
./packs/
├── core/
│ ├── actions/
│ ├── sensors/
│ └── pack.yaml
│
│ ┌─────────────┐
│ (copy during │ packs_data │──────────> /opt/attune/packs (api)
│ init-packs) │ volume │
│ └────────────>│ │──────────> /opt/attune/packs (executor)
│ │ │
│ │ │──────────> /opt/attune/packs (worker)
│ │ │
│ │ │──────────> /opt/attune/packs (sensor)
│ └─────────────┘
│
./packs.dev/
└── custom-pack/ ┌────────────────────────> /opt/attune/packs.dev (all)
(bind mount) │ (read-write for dev)
│
└─ (mounted directly)
Why Volumes Instead of COPY?
| Aspect | COPY into Image | Volume Mount |
|---|---|---|
| Update packs | Rebuild image (~5 min) | Restart service (~5 sec) |
| Image size | Larger (+packs) | Smaller (no packs) |
| Development | Slow iteration | Fast iteration |
| Consistency | Each service separate | All services share |
| Pack binaries | Baked into image | Updateable |
docker-compose.yaml Configuration
volumes:
packs_data:
driver: local
services:
# Step 1: init-packs runs once to populate packs_data volume
init-packs:
image: python:3.11-alpine
volumes:
- ./packs:/source/packs:ro # Host packs (read-only)
- packs_data:/opt/attune/packs # Target volume
command: ["/bin/sh", "/init-packs.sh"]
restart: on-failure
# Step 2: Services mount packs_data as read-only
api:
volumes:
- packs_data:/opt/attune/packs:ro # Production packs (RO)
- ./packs.dev:/opt/attune/packs.dev:rw # Dev packs (RW)
depends_on:
init-packs:
condition: service_completed_successfully
worker-shell:
volumes:
- packs_data:/opt/attune/packs:ro # Same volume
- ./packs.dev:/opt/attune/packs.dev:rw
# ... all services follow same pattern
Pack Binaries (Native Code)
Some packs contain compiled binaries (e.g., sensors written in Rust).
Building Pack Binaries
Option 1: Use the script (recommended)
./scripts/build-pack-binaries.sh
Option 2: Manual build
# Build in Docker with GLIBC compatibility
docker build -f docker/Dockerfile.pack-binaries -t attune-pack-builder .
# Extract binaries
docker create --name pack-tmp attune-pack-builder
docker cp pack-tmp:/pack-binaries/. ./packs/
docker rm pack-tmp
Option 3: Native build (if GLIBC matches)
cargo build --release --bin attune-core-timer-sensor
cp target/release/attune-core-timer-sensor packs/core/sensors/
When to Rebuild Pack Binaries
- ✅ After
git pullthat updates pack binary source - ✅ After modifying sensor source code (e.g.,
crates/core-timer-sensor) - ✅ When setting up development environment for first time
- ❌ NOT needed for YAML/script changes in packs
Development Workflow
Editing Pack YAML Files
# 1. Edit pack files
vim packs/core/actions/echo.yaml
# 2. Restart services (no rebuild!)
docker compose restart
# 3. Test changes
curl -X POST http://localhost:8080/api/v1/executions \
-H "Authorization: Bearer $TOKEN" \
-d '{"action_ref": "core.echo", "parameters": {"message": "hello"}}'
Time: ~5 seconds
Editing Pack Scripts (Python/Shell)
# 1. Edit script
vim packs/core/actions/http_request.py
# 2. Restart services
docker compose restart worker-python
# 3. Test
# (run execution)
Time: ~5 seconds
Editing Pack Binaries (Native Sensors)
# 1. Edit source
vim crates/core-timer-sensor/src/main.rs
# 2. Rebuild binary
./scripts/build-pack-binaries.sh
# 3. Restart services
docker compose restart sensor
# 4. Test
# (check sensor registration)
Time: ~2 minutes (compile + restart)
Development Packs (packs.dev)
For rapid development, use the packs.dev directory:
# Create a dev pack
mkdir -p packs.dev/mypack/actions
# Create action
cat > packs.dev/mypack/actions/test.yaml <<EOF
name: test
description: Test action
runner_type: Shell
entry_point: echo.sh
parameters:
message:
type: string
required: true
EOF
cat > packs.dev/mypack/actions/echo.sh <<'EOF'
#!/bin/bash
echo "Message: $ATTUNE_MESSAGE"
EOF
chmod +x packs.dev/mypack/actions/echo.sh
# Restart to pick up changes
docker compose restart
# Test immediately - no rebuild needed!
Benefits of packs.dev:
- ✅ Direct bind mount (changes visible immediately)
- ✅ Read-write access (can modify from container)
- ✅ No init-packs step needed
- ✅ Perfect for iteration
Optimized Dockerfiles and Packs
The optimized Dockerfiles (docker/Dockerfile.optimized) do NOT copy packs:
# ❌ OLD: Packs copied into image
COPY packs/ ./packs/
# ✅ NEW: Only create mount point
RUN mkdir -p /opt/attune/packs /opt/attune/logs
# Packs mounted at runtime from packs_data volume
Result:
- Service images contain only binaries + configs
- Packs updated independently
- Faster builds (no pack layer invalidation)
Troubleshooting
"Pack not found" errors
Symptom: API returns 404 for pack/action Cause: Packs not loaded into volume
Fix:
# Check if packs exist in volume
docker compose exec api ls -la /opt/attune/packs/
# If empty, restart init-packs
docker compose restart init-packs
docker compose logs init-packs
Pack changes not visible
Symptom: Updated pack.yaml but changes not reflected
Cause: Changes made to host ./packs/ after init-packs ran
Fix:
# Option 1: Use packs.dev for development
mv packs/mypack packs.dev/mypack
docker compose restart
# Option 2: Recreate packs_data volume
docker compose down
docker volume rm attune_packs_data
docker compose up -d
Pack binary "exec format error"
Symptom: Sensor binary fails with exec format error Cause: Binary compiled for wrong architecture or GLIBC version
Fix:
# Rebuild with Docker (ensures compatibility)
./scripts/build-pack-binaries.sh
# Restart sensor service
docker compose restart sensor
Pack binary "permission denied"
Symptom: Binary exists but can't execute Cause: Binary not executable
Fix:
chmod +x packs/core/sensors/attune-core-timer-sensor
docker compose restart init-packs sensor
Best Practices
DO:
- ✅ Use
./scripts/build-pack-binaries.shfor pack binaries - ✅ Put development packs in
packs.dev/ - ✅ Keep production packs in
packs/ - ✅ Commit pack YAML/scripts to git
- ✅ Use
.gitignorefor compiled pack binaries - ✅ Restart services after pack changes
- ✅ Use
init-packslogs to debug loading issues
DON'T:
- ❌ Don't copy packs into Dockerfiles
- ❌ Don't edit packs inside running containers
- ❌ Don't commit compiled pack binaries to git
- ❌ Don't expect instant updates to
packs/(need restart) - ❌ Don't rebuild service images for pack changes
- ❌ Don't modify packs_data volume directly
Migration from Old Dockerfiles
If your old Dockerfiles copied packs:
# OLD Dockerfile
COPY packs/ ./packs/
COPY --from=pack-builder /build/pack-binaries/ ./packs/
Migration steps:
-
Build pack binaries separately:
./scripts/build-pack-binaries.sh -
Update to optimized Dockerfile:
# docker-compose.yaml api: build: dockerfile: docker/Dockerfile.optimized -
Rebuild service images:
docker compose build --no-cache -
Start services (init-packs will populate volume):
docker compose up -d
Summary
Architecture: Packs → Volume → Services
- Host
./packs/copied topacks_datavolume byinit-packs - Services mount
packs_dataas read-only - Dev packs in
packs.dev/bind-mounted directly
Benefits:
- 90% faster pack updates (restart vs rebuild)
- Smaller service images
- Consistent packs across all services
- Clear separation: services = code, packs = content
Key Commands:
./scripts/build-pack-binaries.sh # Build native pack binaries
docker compose restart # Pick up pack changes
docker compose logs init-packs # Debug pack loading
Remember: Packs are content, not code. Treat them as configuration, not part of the service image.