phase: 01-docker-compose-stack
plan: 02
type: execute
wave: 2
depends_on:
- 01-01
files_modified:
- ollama-entrypoint.sh
- docker-compose.yml
- logs/test.log
- smoke_test.sh
autonomous: true
requirements:
- SVC-03
- SVC-04
- SVC-06
- ING-04
- OPS-03
must_haves:
truths:
- "docker compose up starts all three services (app, db, ollama)"
- "Ollama auto-pulls nomic-embed-text on first startup"
- "pgvector data persists across container restarts via named volume"
- "App waits for DB and Ollama to be healthy before starting"
- "Logs are visible via docker compose logs"
artifacts:
- path: "ollama-entrypoint.sh"
provides: "Ollama startup script with model auto-pull"
contains: "ollama pull nomic-embed-text"
- path: "docker-compose.yml"
provides: "Three-service stack definition"
contains: "pgvector/pgvector:pg17"
- path: "logs/test.log"
provides: "Sample log fixture for integration testing"
min_lines: 5
- path: "smoke_test.sh"
provides: "Automated smoke test sequence for all requirements"
contains: "curl"
key_links:
- from: "docker-compose.yml (app service)"
to: "Dockerfile"
via: "build: ."
pattern: "build:\s*\."
- from: "docker-compose.yml (db service)"
to: "schema.sql"
via: "volume mount to /docker-entrypoint-initdb.d/"
pattern: "schema.sql:/docker-entrypoint-initdb.d"
- from: "docker-compose.yml (ollama service)"
to: "ollama-entrypoint.sh"
via: "bind mount as entrypoint script"
pattern: "ollama-entrypoint.sh"
- from: "docker-compose.yml (app service)"
to: ".env"
via: "env_file directive"
pattern: "env_file"
Create docker-compose.yml with all three services (app, db, ollama), the Ollama entrypoint script for model auto-pull, a test log fixture, and a smoke test script.
Purpose: This is the core infrastructure plan -- it wires together the Dockerfile from Plan 01 with PostgreSQL+pgvector and Ollama into a single docker compose up command.
Output: docker-compose.yml, ollama-entrypoint.sh, logs/test.log, smoke_test.sh
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-docker-compose-stack/01-RESEARCH.md
@.planning/phases/01-docker-compose-stack/01-01-SUMMARY.md
@Dockerfile
@.env.example
@schema.sql
@api.py
Dockerfile: FROM python:3.14-slim, WORKDIR /app, EXPOSE 8080, CMD uvicorn api:app --host 0.0.0.0 --port 8080
.env.example: DATABASE_URL=postgresql://rag:rag@db:5432/supportdesk, OLLAMA_URL=http://ollama:11434, POSTGRES_USER=rag, POSTGRES_PASSWORD=rag, POSTGRES_DB=supportdesk
requirements.txt: psycopg2-binary==2.9.11, pgvector==0.4.2
schema.sql: CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS chunks(...)
- .planning/phases/01-docker-compose-stack/01-RESEARCH.md (Pattern 2: Ollama Model Auto-Pull via Entrypoint Script, lines 128-152)
Create ollama-entrypoint.sh in the repo root with the following exact content:
```sh
#!/bin/sh
set -e
# Start Ollama server in background
ollama serve &
SERVE_PID=$!
# Wait for Ollama API to be responsive
echo "Waiting for Ollama API..."
until ollama list >/dev/null 2>&1; do
sleep 2
done
# Pull embedding model (no-op if already cached in volume)
echo "Pulling nomic-embed-text..."
ollama pull nomic-embed-text
echo "Ollama ready with nomic-embed-text."
wait $SERVE_PID
```
This script:
- Starts `ollama serve` in background
- Polls until the API is responsive via `ollama list`
- Pulls nomic-embed-text (274 MB on first run, instant if cached in named volume)
- Waits on the serve process so the container stays alive
Make the file executable: `chmod +x ollama-entrypoint.sh`
- ollama-entrypoint.sh exists in repo root
- ollama-entrypoint.sh is executable (chmod +x)
- ollama-entrypoint.sh contains #!/bin/sh
- ollama-entrypoint.sh contains ollama serve &
- ollama-entrypoint.sh contains ollama pull nomic-embed-text
- ollama-entrypoint.sh contains wait $SERVE_PID
- .planning/phases/01-docker-compose-stack/01-RESEARCH.md (Code Examples section: docker-compose.yml structure, lines 266-318)
- .env.example (container hostnames and POSTGRES_* vars from Plan 01)
- schema.sql (mounted into db container at /docker-entrypoint-initdb.d/)
- Dockerfile (build context for app service from Plan 01)
- ollama-entrypoint.sh (entrypoint for ollama service from Task 1)
Create docker-compose.yml in the repo root with the following exact content:
```yaml
services:
db:
image: pgvector/pgvector:pg17
environment:
POSTGRES_USER: ${POSTGRES_USER:-rag}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-rag}
POSTGRES_DB: ${POSTGRES_DB:-supportdesk}
volumes:
- pgdata:/var/lib/postgresql/data
- ./schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-rag} -d ${POSTGRES_DB:-supportdesk}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
restart: unless-stopped
ollama:
image: ollama/ollama:latest
volumes:
- ollama_data:/root/.ollama
- ./ollama-entrypoint.sh:/ollama-entrypoint.sh:ro
entrypoint: ["/bin/sh", "/ollama-entrypoint.sh"]
healthcheck:
test: ["CMD-SHELL", "ollama list | grep -q nomic-embed-text"]
interval: 10s
timeout: 10s
retries: 30
start_period: 60s
restart: unless-stopped
app:
build: .
ports:
- "8080:8080"
env_file: .env
volumes:
- ./logs:/app/logs
depends_on:
db:
condition: service_healthy
ollama:
condition: service_healthy
restart: unless-stopped
volumes:
pgdata:
ollama_data:
```
Key design decisions:
- NO `version:` field (Docker Compose v2 ignores it -- per RESEARCH.md "Deprecated/outdated")
- db: pgvector/pgvector:pg17 (NOT ankane/pgvector). Healthcheck uses pg_isready. schema.sql mounted read-only at /docker-entrypoint-initdb.d/01-schema.sql for auto-init on first run.
- ollama: ollama-entrypoint.sh bind-mounted read-only. Healthcheck verifies nomic-embed-text is loaded (not just server up). start_period: 60s and retries: 30 allow for first-time model pull (274 MB).
- app: builds from Dockerfile in repo root. Port 8080:8080. Reads .env file. logs/ bind-mounted for ingest. depends_on db AND ollama with service_healthy condition.
- Named volumes: pgdata (survives restarts = SVC-04), ollama_data (caches model = avoids re-pull)
- restart: unless-stopped on all services for resilience
- docker-compose.yml exists in repo root
- docker-compose.yml does NOT contain version:
- db service uses image pgvector/pgvector:pg17
- db service has healthcheck with pg_isready
- db service mounts ./schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
- db service has volume pgdata:/var/lib/postgresql/data
- ollama service uses image ollama/ollama:latest
- ollama service has entrypoint ["/bin/sh", "/ollama-entrypoint.sh"]
- ollama service has healthcheck with ollama list | grep -q nomic-embed-text
- ollama service has volume ollama_data:/root/.ollama
- ollama service mounts ./ollama-entrypoint.sh:/ollama-entrypoint.sh:ro
- app service has build: .
- app service has ports: - "8080:8080"
- app service has env_file: .env
- app service has depends_on with condition: service_healthy for both db and ollama
- app service has volume ./logs:/app/logs
- Top-level volumes section declares pgdata: and ollama_data:
- .planning/phases/01-docker-compose-stack/01-VALIDATION.md (Wave 0 Requirements: logs/test.log, smoke_test.sh)
- .planning/phases/01-docker-compose-stack/01-RESEARCH.md (Validation Architecture section, lines 431-463)
- api.py (POST /ingest accepts {"filename": "logs/test.log"}, POST /query accepts {"question": "..."}, GET /health)
1. Create directory logs/ if it does not exist.
2. Create `logs/test.log` with realistic sample log content (at least 25 lines so it produces 2+ chunks at CHUNK_LINES=20):
```
2026-03-22 08:01:12 INFO mailserver01 postfix/smtpd[12345]: connect from relay.example.com[10.0.1.5]
2026-03-22 08:01:13 INFO mailserver01 postfix/smtpd[12345]: NOQUEUE: reject: RCPT from relay.example.com[10.0.1.5]: 550 5.1.1 <unknown@example.com>: Recipient address rejected
2026-03-22 08:01:14 INFO mailserver01 postfix/smtpd[12345]: disconnect from relay.example.com[10.0.1.5]
2026-03-22 08:05:22 WARN webproxy01 squid[8821]: WARNING: DNS lookup for internal.corp failed
2026-03-22 08:05:23 ERROR webproxy01 squid[8821]: comm_open: socket failure: (97) Address family not supported
2026-03-22 08:10:00 INFO backup01 rsync[4412]: sending incremental file list
2026-03-22 08:10:05 INFO backup01 rsync[4412]: sent 1,234,567 bytes received 890 bytes
2026-03-22 08:10:06 INFO backup01 rsync[4412]: total size is 45,678,901 speedup is 36.99
2026-03-22 08:15:30 ERROR firewall01 iptables: INPUT DROP: SRC=192.168.1.100 DST=10.0.0.1 PROTO=TCP DPT=22
2026-03-22 08:15:31 ERROR firewall01 iptables: INPUT DROP: SRC=192.168.1.100 DST=10.0.0.1 PROTO=TCP DPT=23
2026-03-22 08:15:32 WARN firewall01 iptables: rate-limit reached for SSH brute-force rule
2026-03-22 08:20:00 INFO monitoring nagios[2201]: SERVICE ALERT: mailserver01;SMTP;OK;HARD;3;SMTP OK - 0.023 second response time
2026-03-22 08:20:01 INFO monitoring nagios[2201]: SERVICE ALERT: webproxy01;HTTP;WARNING;SOFT;1;HTTP WARNING: HTTP/1.1 503 Service Unavailable
2026-03-22 08:25:10 INFO appserver01 java[9901]: Deployment of webapp.war completed in 12.3s
2026-03-22 08:25:11 WARN appserver01 java[9901]: Memory usage at 78% of max heap (3.1GB/4GB)
2026-03-22 08:25:12 INFO appserver01 java[9901]: Active sessions: 142, Peak today: 312
2026-03-22 08:30:00 ERROR database01 mysql[3306]: [ERROR] InnoDB: Unable to lock ./ibdata1 error: 11
2026-03-22 08:30:01 ERROR database01 mysql[3306]: [ERROR] InnoDB: Check that you do not already have another mysqld process using the same InnoDB data or log files
2026-03-22 08:30:02 INFO database01 mysql[3306]: [Note] InnoDB: Retrying lock on ./ibdata1
2026-03-22 08:30:05 INFO database01 mysql[3306]: [Note] InnoDB: Lock acquired successfully
2026-03-22 08:35:00 INFO loadbalancer haproxy[1100]: Server backend/web1 is UP, reason: Layer7 check passed
2026-03-22 08:35:01 WARN loadbalancer haproxy[1100]: Server backend/web2 is DOWN, reason: Layer4 timeout
2026-03-22 08:35:02 INFO loadbalancer haproxy[1100]: backend has 2/3 active servers
2026-03-22 08:40:00 INFO dns01 named[5500]: zone example.com/IN: refresh: retry limit for master 10.0.0.53 exceeded
2026-03-22 08:40:01 WARN dns01 named[5500]: zone example.com/IN: Transfer started.
```
3. Create `smoke_test.sh` in the repo root with the following content:
```bash
#!/usr/bin/env bash
# smoke_test.sh -- Phase 1 smoke tests for RAG Support Desk Docker stack
# Usage: bash smoke_test.sh
set -euo pipefail
PASS=0
FAIL=0
BASE="http://localhost:8080"
check() {
local name="$1"; shift
if "$@" >/dev/null 2>&1; then
echo "[PASS] $name"
PASS=$((PASS + 1))
else
echo "[FAIL] $name"
FAIL=$((FAIL + 1))
fi
}
echo "=== RAG Support Desk Smoke Tests ==="
echo ""
# SVC-06: Health endpoint responds
check "SVC-06: Health endpoint" curl -sf "$BASE/health"
# SVC-06: Health returns ok status
check "SVC-06: Health returns ok" bash -c "curl -sf $BASE/health | grep -q ok"
# OPS-03: App logs contain uvicorn output
check "OPS-03: App logs visible" docker compose logs app --no-color 2>&1 | grep -q "uvicorn"
# SVC-03: Ollama has nomic-embed-text
check "SVC-03: Ollama model loaded" docker compose exec -T ollama ollama list 2>&1 | grep -q "nomic-embed-text"
# SVC-04: Data survives restart
check "SVC-04: DB survives restart" bash -c "docker compose restart db && sleep 5 && curl -sf $BASE/health"
# ING-04: Ingest endpoint accepts test log
check "ING-04: Ingest test.log" curl -sf -X POST "$BASE/ingest" \
-H "Content-Type: application/json" \
-d '{"filename":"logs/test.log"}'
# QRY: Query endpoint responds (requires ANTHROPIC_API_KEY)
check "QRY: Query responds" curl -sf -X POST "$BASE/query" \
-H "Content-Type: application/json" \
-d '{"question":"Was ist mit dem Mailserver passiert?"}'
echo ""
echo "=== Results: $PASS passed, $FAIL failed ==="
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
```
Make smoke_test.sh executable: `chmod +x smoke_test.sh`
- logs/test.log exists with at least 25 lines of realistic log content
- logs/test.log contains entries from multiple systems (mailserver, webproxy, firewall, database, etc.)
- smoke_test.sh exists in repo root and is executable
- smoke_test.sh tests health endpoint (SVC-06)
- smoke_test.sh tests docker compose logs (OPS-03)
- smoke_test.sh tests ollama model presence (SVC-03)
- smoke_test.sh tests DB restart persistence (SVC-04)
- smoke_test.sh tests ingest with logs/test.log (ING-04)
- smoke_test.sh exits 0 on all pass, 1 on any failure
- docker compose config validates the compose file syntax
- ls -la ollama-entrypoint.sh shows executable permissions
- wc -l logs/test.log shows 25+ lines
- bash -n smoke_test.sh validates shell syntax
- docker-compose.yml defines three services with healthchecks and named volumes
- ollama-entrypoint.sh auto-pulls nomic-embed-text
- logs/test.log provides realistic test data for ingest
- smoke_test.sh validates all Phase 1 requirements