# Upgrade and migration

> Pull new images, automatic DB migrations on API startup, compose project rename volume migration, and version upgrade verification signals.

- Repository: Paca-AI/paca
- GitHub: https://github.com/Paca-AI/paca
- Human docs: https://grok-wiki.com/public/docs/paca-ai-paca-f238b2ab3d25
- Complete Markdown: https://grok-wiki.com/public/docs/paca-ai-paca-f238b2ab3d25/llms-full.txt

## Source Files

- `deploy/README.md`
- `docs/guides/getting-started.md`
- `services/api/docker-entrypoint.sh`
- `deploy/docker-compose.prod.yml`
- `README.md`

---

---
title: "Upgrade and migration"
description: "Pull new images, automatic DB migrations on API startup, compose project rename volume migration, and version upgrade verification signals."
---

Production upgrades are image pulls plus a Compose restart: the API container applies embedded core schema SQL on every startup before serving traffic, and plugin SQL runs per installed plugin. Persistent state lives in Compose-named volumes keyed by project name (`paca`); installations that used the older `paca-prod` project require explicit volume copies.

## Upgrade paths

| Deployment | Typical upgrade command | Schema migration owner |
|---|---|---|
| Production Compose (self-hosted) | `docker compose pull` then `docker compose --env-file .env up -d` | API binary on startup |
| Install script layout | Same, from the install directory | API binary on startup |
| Contributor dev stack | `git pull` then restart API (container or host) | API binary on startup |
| Fresh Postgres in dev Compose | Postgres `initdb` scripts on first volume init only | Postgres init + API on subsequent restarts |

<Warning>
Keep your existing `.env` across upgrades. Changing `JWT_SECRET` invalidates sessions; changing `ENCRYPTION_KEY` breaks decryption of stored plugin and agent secrets. `ADMIN_PASSWORD` only affects the seeded admin account on first bootstrap, not ongoing logins.
</Warning>

## Standard production upgrade

Run from the directory that contains `docker-compose.yml` and `.env` (for example `./paca` after `install.sh`).

<Steps>
<Step title="Pull new images">

```bash
docker compose pull
```

This fetches updated tags for `api`, `web`, `realtime`, `ai-agent`, and infrastructure images defined in the compose file.

</Step>
<Step title="Recreate containers">

```bash
docker compose --env-file .env up -d
```

Compose recreates changed services. The API container restarts and runs core migrations before binding port 8080.

If you scaled optional services, pass the same `--scale` flags used at install time (for example `--scale postgres=0` for external PostgreSQL).

</Step>
<Step title="Verify health">

Confirm the stack is healthy before handing traffic back to users. See [Verification signals](#verification-signals) below.

</Step>
</Steps>

<Tip>
The install script starts with `--pull always`. For manual upgrades, `docker compose pull` followed by `up -d` is equivalent.
</Tip>

## Pin or lock a release

Image tags are controlled through `.env` variables. The install script writes pinned tags from the chosen release; manual deployments default to `:latest`.

| Variable | Default (prod compose) | Example pinned value |
|---|---|---|
| `PACA_API_IMAGE` | `pacaai/paca-api:latest` | `pacaai/paca-api:0.4.0` |
| `PACA_WEB_IMAGE` | `pacaai/paca-web:latest` | `pacaai/paca-web:0.4.0` |
| `PACA_REALTIME_IMAGE` | `pacaai/paca-realtime:latest` | `pacaai/paca-realtime:0.4.0` |
| `PACA_AI_AGENT_IMAGE` | `pacaai/paca-ai-agent:latest` | `pacaai/paca-ai-agent:0.4.0` |

To install a specific release via the install script:

```bash
PACA_VERSION=v0.4.0 bash install.sh
```

After editing image variables, run `docker compose pull` and `docker compose --env-file .env up -d` again.

## Automatic core database migrations

Core schema changes ship inside the API Docker image. On every startup, bootstrap runs embedded SQL from `services/api/migrations/` before seeding admin data or loading plugins.

```text
API container start
       │
       ▼
bootstrap.New()
       │
       ├── connect PostgreSQL + Valkey
       ├── RunMigrationsFS(db, migrations.FS)   ← all *.sql, lexicographic order
       ├── seed roles / admin / agent bot
       ├── run plugin migrations (per enabled plugin)
       └── load WASM plugins + start HTTP server
```

Migration properties:

| Property | Behavior |
|---|---|
| Execution timing | Every API startup |
| File order | Lexicographic filename sort (`000001_init.sql` … `000012_…`) |
| Idempotency | SQL uses `CREATE TABLE IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`, `INSERT … ON CONFLICT`, and similar patterns |
| Failure mode | Bootstrap returns an error; the API process exits with `bootstrap: auto-migrate: …` and the container restarts |
| Runtime dependency | No source tree or migration directory mount required in production |

<Note>
Core migrations are not tracked in a `schema_migrations` table. Idempotent re-execution on each startup is the versioning mechanism. New release images embed new `*.sql` files that apply additive changes.
</Note>

### Manual migration (development)

Contributors can apply the same SQL files against a running database:

```bash
cd services/api
export DATABASE_URL=postgres://paca:paca@localhost:5432/paca?sslmode=disable
make migrate-up
```

`make migrate-up` runs `psql -f` on each `migrations/*.sql` file in sorted order. Use this when debugging migration SQL or when the API is not the migration runner.

In `deploy/docker-compose.dev.yml`, Postgres mounts `services/api/migrations` to `/docker-entrypoint-initdb.d` for **first-time volume initialization only**. Ongoing schema changes still rely on API startup migrations (or `make migrate-up`).

## Plugin migrations

Plugin-owned SQL is separate from core migrations. Each plugin gets a dedicated PostgreSQL schema and a `plugin_schema_migrations` tracking table. Only files not yet recorded are applied, in lexicographic order, inside per-file transactions.

Plugin migrations run in three contexts:

| Context | When | On failure |
|---|---|---|
| API startup | For each **enabled** installed plugin, before WASM load | Logged as `plugin: migration failed`; API still starts |
| Marketplace install | After artifact download, before runtime load | Install rolled back; API returns error |
| Marketplace upgrade | After new artifacts, before version persist | Artifacts cleaned up; API returns error |

Plugin WASM, frontend, and MCP artifacts persist in Compose volumes (`backend_plugins`, `frontend_plugins`, `mcp_plugins`). Upgrading the Paca platform image does not reinstall or upgrade marketplace plugins — upgrade plugins separately through the admin UI or plugin API.

## Compose project rename volume migration

`deploy/docker-compose.prod.yml` sets `name: paca`. Docker Compose prefixes volume names with the project name, so data volumes are `paca_postgres_data`, `paca_valkey_data`, `paca_minio_data`, `paca_backend_plugins`, `paca_frontend_plugins`, and `paca_mcp_plugins`.

Earlier installations used project name `paca-prod`, producing volumes like `paca-prod_postgres_data`. A normal `docker compose up` under the new project name creates **empty** volumes and looks like a fresh install.

Migrate data only when you need to preserve an existing installation:

<Steps>
<Step title="Stop the old stack">

```bash
docker compose -p paca-prod --env-file .env down
```

Volumes remain on disk.

</Step>
<Step title="Copy each volume">

Example for PostgreSQL:

```bash
docker volume create paca_postgres_data
docker run --rm \
  -v paca-prod_postgres_data:/from \
  -v paca_postgres_data:/to \
  alpine sh -c "cp -av /from/. /to/"
docker volume rm paca-prod_postgres_data
```

Repeat for `minio_data`, `valkey_data`, `backend_plugins`, `frontend_plugins`, and `mcp_plugins` as needed. Map `paca-prod_<suffix>` → `paca_<suffix>`.

</Step>
<Step title="Start under the new project name">

```bash
docker compose --env-file .env up -d
```

The API applies any new core migrations against the copied database.

</Step>
</Steps>

<Check>
Fresh installs skip volume migration entirely. Only rename when upgrading an existing `paca-prod` deployment that must keep data.
</Check>

## Verification signals

Paca exposes no dedicated `/version` endpoint. Confirm a successful upgrade through container health, HTTP probes, logs, and UI smoke checks.

### HTTP health endpoints

| Probe | Path | Auth | Expected response |
|---|---|---|---|
| API (direct or via gateway) | `GET /api/healthz` | None | `200` with `{"status":"ok"}` |
| Realtime (internal) | `GET /healthz` on port 3001 | None | `200` with `{"status":"ok"}` |
| Gateway (E2E compose pattern) | `GET /api/healthz` through port 80 | None | `200` with `{"status":"ok"}` |

<RequestExample>

```bash
curl -fsS http://localhost/api/healthz
```

</RequestExample>

<ResponseExample>

```json
{"status":"ok"}
```

</ResponseExample>

### Docker Compose health checks

Production `docker-compose.prod.yml` defines health checks that gate dependent services:

| Service | Health check target | Notes |
|---|---|---|
| `postgres` | `pg_isready` | API `depends_on` with `service_healthy` |
| `valkey` | `valkey-cli ping` | API and realtime wait for healthy |
| `minio` | `mc ready local` | API waits when MinIO is enabled |
| `api` | `wget` → `http://localhost:8080/api/healthz` | `start_period: 40s` allows migration + bootstrap time |

When `api` becomes healthy, `realtime` and `gateway` start. A stuck `api` container in `starting` state usually indicates a migration or bootstrap failure.

### Log signals

After a successful core migration pass, API logs include:

```text
schema migrations applied
```

Plugin migrations log per file:

```text
plugin: applying migration  plugin=<name> file=<filename>
plugin: migration applied   plugin=<name> file=<filename>
```

Inspect API logs when health checks fail:

```bash
docker compose logs api --tail=100
```

### Post-upgrade smoke checks

| Check | Pass criteria |
|---|---|
| Web UI | Login page loads at `PUBLIC_URL` |
| Authenticated API | Existing sessions work (if `JWT_SECRET` unchanged) or fresh login succeeds |
| Realtime | Board or task updates propagate without refresh |
| Plugins | Installed plugins appear in Settings; no migration errors in API logs |
| Object storage | File attachments upload and download |
| AI agent (if enabled) | Agent conversations start without auth errors between `AGENT_API_KEY` and `INTERNAL_API_KEY` |

## Contributor stack upgrades

For local development with `deploy/docker-compose.dev.yml` (project name `paca-dev`):

<Steps>
<Step title="Update source">

```bash
git pull
```

</Step>
<Step title="Restart application services">

If running in containers:

```bash
docker compose -f deploy/docker-compose.dev.yml restart api web realtime
```

If running API on the host (`make run` or `air`), stop and restart the process. Migrations run on the next bootstrap.

</Step>
<Step title="Rebuild when base images changed">

```bash
docker compose -f deploy/docker-compose.dev.yml up -d --build api realtime
```

</Step>
</Steps>

Dev Postgres data persists in the `paca-dev_postgres_data` volume across restarts. To reset completely:

```bash
docker compose -f deploy/docker-compose.dev.yml down -v
```

## Failure modes

| Symptom | Likely cause | Direction |
|---|---|---|
| `api` stuck unhealthy / restart loop | Core migration SQL error | Read `docker compose logs api`; fix DB state or roll back image tag |
| Empty database after upgrade | New Compose project name without volume copy | Run [volume migration](#compose-project-renamed-volume-migration) or restore backup |
| Sessions invalidated | `JWT_SECRET` changed in `.env` | Expected; users re-login |
| Plugin secrets / agent LLM keys broken | `ENCRYPTION_KEY` changed | Restore previous key from backup; data cannot be re-decrypted otherwise |
| Plugin features missing | Plugin migration failed at startup (logged, non-fatal) | Check logs; reinstall or upgrade plugin from marketplace |
| `401` on agent tasks | `AGENT_API_KEY` / `INTERNAL_API_KEY` mismatch after `.env` edit | Align keys across `api` and `ai-agent` services, restart both |

## Related pages

<CardGroup>
<Card title="Deploy production" href="/deploy-production">
Production Compose topology, gateway routing, storage backends, and secret generation.
</Card>
<Card title="Installation" href="/installation">
First-time install script and manual Compose setup prerequisites.
</Card>
<Card title="Quickstart" href="/quickstart">
Health endpoint verification and first successful login flow.
</Card>
<Card title="Install marketplace plugins" href="/install-marketplace-plugins">
Plugin install and upgrade lifecycle including per-plugin SQL migrations.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Image variables, database URLs, and encryption keys that must stay stable across upgrades.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Health check failures, Valkey connectivity, and auth cookie issues after restarts.
</Card>
</CardGroup>
