Files
simbarag/docs/deployment.md
T
ryan d8d01a5fff Zero-downtime blue-green deploys behind Caddy health checks
Split the raggr service into raggr-blue (8080) and raggr-green (8081)
behind compose profiles, add a /api/health endpoint and container
healthcheck, and add scripts/deploy.sh to build the inactive color,
wait for it to become healthy, then gracefully stop the old one.
Makefile targets resolve the active color automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-10 01:41:37 -04:00

257 lines
7.7 KiB
Markdown

# Deployment & Migrations Guide
This document covers database migrations and deployment workflows for SimbaRAG.
## Migration Workflow
Migrations are managed by [Aerich](https://github.com/tortoise/aerich), the migration tool for Tortoise ORM.
### Key Principles
1. **Generate migrations in Docker** - Aerich needs database access to detect schema changes
2. **Migrations auto-apply on startup** - Both `startup.sh` and `startup-dev.sh` run `aerich upgrade`
3. **Commit migrations to git** - Migration files must be in the repo for production deploys
### Generating a New Migration
#### Development (Recommended)
With `docker-compose.dev.yml`, your local `services/raggr` directory is synced to the container. Migrations generated inside the container appear on your host automatically.
```bash
# 1. Start the dev environment
docker compose -f docker-compose.dev.yml up -d
# 2. Generate migration (runs inside container, syncs to host)
docker compose -f docker-compose.dev.yml exec raggr aerich migrate --name describe_your_change
# 3. Verify migration was created
ls services/raggr/migrations/models/
# 4. Commit the migration
git add services/raggr/migrations/
git commit -m "Add migration: describe_your_change"
```
#### Production Container
For production, migration files are baked into the image. You must generate migrations in dev first.
```bash
# If you need to generate a migration from production (not recommended):
docker compose exec raggr aerich migrate --name describe_your_change
# Copy the file out of the container
docker cp $(docker compose ps -q raggr):/app/migrations/models/ ./services/raggr/migrations/
```
### Applying Migrations
Migrations apply automatically on container start via the startup scripts.
**Manual application (if needed):**
```bash
# Dev
docker compose -f docker-compose.dev.yml exec raggr aerich upgrade
# Production
docker compose exec raggr aerich upgrade
```
### Checking Migration Status
```bash
# View applied migrations
docker compose exec raggr aerich history
# View pending migrations
docker compose exec raggr aerich heads
```
### Rolling Back
```bash
# Downgrade one migration
docker compose exec raggr aerich downgrade
# Downgrade to specific version
docker compose exec raggr aerich downgrade -v 1
```
## Deployment Workflows
### Development
```bash
# Start with watch mode (auto-restarts on file changes)
docker compose -f docker-compose.dev.yml up
# Or with docker compose watch (requires Docker Compose v2.22+)
docker compose -f docker-compose.dev.yml watch
```
The dev environment:
- Syncs `services/raggr/` to `/app` in the container
- Rebuilds frontend on changes
- Auto-applies migrations on startup
### Production (zero-downtime blue-green)
The app runs as one of two identical compose services: `raggr-blue`
(host port 8080) or `raggr-green` (host port 8081), gated behind compose
profiles so only one color runs at a time. Caddy health-checks both ports and
routes traffic to whichever is healthy.
```bash
# Pull latest and swap colors with zero downtime
make redeploy
# Or run the swap directly
./scripts/deploy.sh
# View logs / exec into the active color (Makefile resolves the color)
make logs
make migrate-history
```
`scripts/deploy.sh` does the following:
1. Detects the running color via `docker compose ps`.
2. Builds the inactive color.
3. Starts it with `docker compose up -d --wait`, which blocks until the
container healthcheck (`GET /api/health`) passes.
4. If the new color never becomes healthy, it is stopped and the old color
keeps serving (the script exits non-zero).
5. Otherwise the old color is stopped with a 30s grace period so in-flight
SSE streams can finish, then removed.
To target a color manually, enable its profile:
```bash
docker compose --profile blue up -d raggr-blue
docker compose --profile blue --profile green ps
```
#### Caddy configuration
Caddy needs both upstreams with active health checks:
```caddyfile
ask.torrtle.co {
reverse_proxy host.docker.internal:8080 host.docker.internal:8081 {
lb_policy first # prefer blue when both are healthy
lb_try_duration 5s # retry on the other upstream during the swap
lb_try_interval 250ms
health_uri /api/health
health_interval 2s
health_timeout 2s
fail_duration 10s
}
}
```
#### One-time cutover from the old `raggr` service
The first deploy after this change lands needs manual steps, because the old
`raggr` container still holds port 8080 and the deploy script doesn't know
about it:
```bash
# 1. Update the Caddy config to the two-upstream block below and reload Caddy
# 2. On the server:
git pull
docker compose --profile green up -d --build --wait raggr-green # port 8081, no conflict
docker stop -t 30 $(docker ps -q --filter name=raggr-1) # old raggr container
docker rm $(docker ps -aq --filter name=raggr-1)
# From here on, `make redeploy` handles swaps (green is now the active color)
```
#### Blue-green caveats
- **Migrations must be backward-compatible.** The new color runs
`aerich upgrade` on startup while the old color is still serving the
previous code. Additive changes (new tables/columns) are safe; destructive
changes (drop/rename a column the old code reads) need a two-step deploy:
ship code that stops using the column first, then drop it in a later deploy.
- **Obsidian sync overlap.** During the ~30s swap window both containers run
pull-only `ob sync` against the shared `./obvault` mount. Both are
read-only pulls, so this is low risk, but avoid deploying mid-reindex.
### Fresh Deploy (New Database)
On first deploy with an empty database, `startup-dev.sh` runs `aerich init-db` instead of `aerich upgrade`. This creates all tables from the current models.
For production (`startup.sh`), ensure the database exists and run:
```bash
# If aerich table doesn't exist yet
docker compose exec raggr aerich init-db
# Or if migrating from existing schema
docker compose exec raggr aerich upgrade
```
## Troubleshooting
### "No migrations found" on startup
The `migrations/models/` directory is empty or not copied into the image.
**Fix:** Ensure migrations are committed and the Dockerfile copies them:
```dockerfile
COPY migrations ./migrations
```
### Migration fails with "relation already exists"
The database has tables but aerich doesn't know about them (fresh aerich setup on existing DB).
**Fix:** Fake the initial migration:
```bash
# Mark initial migration as applied without running it
docker compose exec raggr aerich upgrade --fake
```
### Model changes not detected
Aerich compares models against the last migration's state. If state is out of sync:
```bash
# Regenerate migration state (dangerous - review carefully)
docker compose exec raggr aerich migrate --name fix_state
```
### Database connection errors
Ensure PostgreSQL is healthy before running migrations:
```bash
# Check postgres status
docker compose ps postgres
# Wait for postgres then run migrations
docker compose exec raggr bash -c "sleep 5 && aerich upgrade"
```
## File Reference
| File | Purpose |
|------|---------|
| `pyproject.toml` | Aerich config (`[tool.aerich]` section) |
| `migrations/models/` | Migration files |
| `startup.sh` | Production startup (runs `aerich upgrade`) |
| `startup-dev.sh` | Dev startup (runs `aerich upgrade` or `init-db`) |
| `app.py` | Contains `TORTOISE_CONFIG` |
| `aerich_config.py` | Aerich initialization configuration |
## Quick Reference
| Task | Command |
|------|---------|
| Generate migration | `docker compose -f docker-compose.dev.yml exec raggr aerich migrate --name name` |
| Apply migrations | `docker compose exec raggr aerich upgrade` |
| View history | `docker compose exec raggr aerich history` |
| Rollback | `docker compose exec raggr aerich downgrade` |
| Fresh init | `docker compose exec raggr aerich init-db` |