Skip to main content

Service Architecture

Attaché uses Docker Compose as its service layer. The base platform ships required services (Supabase). Skills can add optional services via their own compose files. Each skill runs its own independent compose project — no merging, no assembly.

Design Principles

One compose file per concern. The base platform has its own docker-compose.yml for Supabase. Each skill that needs infrastructure has its own at the skill root. User config repos can add one too. They're all independent projects — no merging, no assembly.

Ansible orchestrates, Compose runs. Ansible's job is discovering compose files and running docker compose up. Compose handles the actual container lifecycle — networking, volumes, health checks, restarts. This separation means you can always docker compose directly for debugging.

Skills own their services. If a skill needs SonarQube, it ships a docker-compose.yml. Attaché doesn't know or care what SonarQube is. It just sees a compose file, starts the containers, and moves on.

Everything survives restarts. Every service uses restart: unless-stopped so Docker brings them back when Colima starts. Colima itself runs as a launchd agent that starts on boot. The only way a service stays down is if you explicitly docker compose down it.

Graceful degradation is the norm. If a service isn't running, skills that depend on it skip those strategies and report what was unavailable. A missing SonarQube container doesn't crash the code review skill — it just means static analysis is skipped while native review and Claude Code dispatch still run.

Base Services

Every Attaché agent runs these services. They're defined in the base platform's compose file:

# ~/.attache/base/docker-compose.yml
services:
supabase-db:
image: supabase/postgres:15.6.1.143
container_name: attache-supabase-db
ports:
- "${SUPABASE_DB_PORT:-65432}:5432"
volumes:
- supabase-db-data:/var/lib/postgresql/data
- ./services/supabase/migrations:/docker-entrypoint-initdb.d
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: postgres
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5

supabase-studio:
image: supabase/studio:latest
container_name: attache-supabase-studio
ports:
- "${SUPABASE_STUDIO_PORT:-65433}:3000"
environment:
STUDIO_PG_META_URL: http://supabase-meta:8080
SUPABASE_URL: http://supabase-kong:8000
depends_on:
supabase-db:
condition: service_healthy
restart: unless-stopped

volumes:
supabase-db-data:

What Supabase Provides

Supabase is the backbone for all structured data:

LayerWhat it storesPostgres features used
Knowledgebasic-memory entities, relations, embeddingspgvector, tsvector, pg_trgm
ActivitySlack messages, meeting transcripts, calendar eventsFTS, JSONB
IdentityPeople crosslinks, match candidates (MDM)Foreign keys, indexes
Agent stateSession metadata, credential referencesStandard tables

The base platform runs migrations on first boot to set up the required schemas, extensions (pgvector, pg_trgm), and tables.

Skill Services

Each skill with infrastructure needs ships a docker-compose.yml at the skill root. It runs as its own independent compose project.

skills/
└── code-review/
├── SKILL.md
├── attache.config.json
├── docker-compose.yml # right at the root, not buried
└── scripts/
└── review.ts

Example: Code Review Skill

# skills/code-review/docker-compose.yml
services:
sonarqube:
image: sonarqube:community
container_name: attache-sonarqube
ports:
- "9000:9000"
volumes:
- sonarqube-data:/opt/sonarqube/data
environment:
- SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true
restart: unless-stopped

volumes:
sonarqube-data:

Example: Monitoring Skill

# skills/monitoring/docker-compose.yml
services:
grafana:
image: grafana/grafana-oss
container_name: attache-grafana
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
restart: unless-stopped

prometheus:
image: prom/prometheus
container_name: attache-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
restart: unless-stopped

volumes:
grafana-data:

Three Tiers, Same Convention

Docker Compose files can exist at three levels, all running independently:

TierLocationPurposeRequired?
Baseattache-platform/docker-compose.ymlSupabase, core infrastructureYes
User<config-repo>/docker-compose.ymlUser's extra services (Redis, Ollama, etc.)No
Skillskills/<name>/docker-compose.ymlSkill-specific services (SonarQube, Grafana, etc.)No

Each runs as its own compose project. No merging, no multi-file assembly:

# Start base services
cd ~/.attache/base && docker compose up -d

# Start a skill's services
cd ~/.openclaw/skills/code-review && docker compose up -d

# Update a skill's services (without touching anything else)
cd ~/.openclaw/skills/code-review && docker compose pull && docker compose up -d

# Remove a skill's services
cd ~/.openclaw/skills/code-review && docker compose down -v

# Check what's running across all projects
docker ps --filter "name=attache-"

This means installing, updating, or removing a skill's infrastructure never affects the base platform or other skills.

Container Naming Convention

All Attaché-managed containers use the attache- prefix:

attache-supabase-db
attache-supabase-studio
attache-sonarqube
attache-grafana

This avoids conflicts with any other Docker workloads on the machine and makes it easy to list all Attaché services with docker ps --filter "name=attache-".

Environment Variables

Service configuration uses environment variables, stored in ~/.attache/.env:

# Base services
POSTGRES_PASSWORD=<generated-on-first-boot>
SUPABASE_DB_PORT=65432
SUPABASE_STUDIO_PORT=65433

# Skill services (added as skills are installed)
SONAR_HOST_URL=http://localhost:9000
SONAR_TOKEN=<generated-after-sonarqube-init>

Ansible generates the .env file during bootstrap. Skill infra playbooks append their variables.

Docker Runtime

Attaché uses Colima as the Docker runtime on macOS — lightweight, CLI-native, no GUI dependency:

# ansible/roles/docker/defaults/main.yml
docker_runtime: colima
colima_cpu: 4
colima_memory: 8
colima_disk: 60
colima_mount_type: virtiofs

The base platform installs Colima via Homebrew and starts it with appropriate resource limits. Docker Desktop is explicitly not used — it has permission issues with agent user home directories and requires a GUI session.

Surviving Restarts

Every service must survive a machine reboot. This requires two things:

1. Colima Starts on Boot

Ansible installs a launchd agent that starts Colima automatically:

<!-- ~/Library/LaunchAgents/com.attache.colima.plist -->
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.attache.colima</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/colima</string>
<string>start</string>
<string>--foreground</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>

2. Containers Restart Automatically

Every docker-compose.yml (base and skill) must use restart: unless-stopped on all services. This ensures Docker restarts them when Colima comes up after a reboot.

The only way a service stays down is if you explicitly docker compose down it.

Lifecycle Commands

# Base platform
cd ~/.attache/base && docker compose up -d
cd ~/.attache/base && docker compose down
cd ~/.attache/base && docker compose logs -f supabase-db

# Skill services (each skill independently)
cd skills/code-review && docker compose up -d
cd skills/code-review && docker compose down
cd skills/code-review && docker compose logs -f sonarqube

# See all Attaché services
docker ps --filter "name=attache-"

Ansible's Role

Ansible handles the orchestration of Docker Compose, not the containers themselves:

Ansible doesDocker Compose does
Install Colima + Docker CLIRun containers
Discover skill compose filesNetwork between services
Generate .env with credentialsMount volumes
Run docker compose up -dHealth checks
Run migrations after DB is healthyRestart policies

This separation means you can always docker compose directly for debugging, and Ansible re-runs are idempotent.