Zum Inhalt springen

Upgrading an existing host

Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.

A host deployed from master before these changes runs seven services and one /opt/pia/.env.prod. The current stack runs twelve, across two environment files, and adds a Temporal backend for the operator runtime. Everything that needs doing is on the host — the images take care of themselves.

Run docker compose -f docker-compose.prod.yml ps first. If there is no pia-migrate in the output, this page applies to you.

Before After
Environment files .env.prod .env.prod + .env.prod.server
Migrations in-process, on pia-server startup the pia-migrate one-shot, gating pia-server
Files on the host compose, Caddyfile, init-db.sh those three + temporal-dynamicconfig.yaml
Compose invocation plain -f docker-compose.prod.yml --env-file .env.prod -f docker-compose.prod.yml
Services 7 12, four of which are one-shots that exit
  1. Back up.

    Terminal window
    cd /opt/pia
    cp .env.prod .env.prod.backup
    docker inspect --format '{{index .RepoDigests 0}}' ghcr.io/pia-ai-dev/pia-server:latest
    docker compose -f docker-compose.prod.yml exec postgres pg_dumpall -U pia > /root/pia-preupgrade.sql

    Keep the digest. It is what a rollback pins to, since the tag is :latest.

  2. Create .env.prod.server. Copy .env.prod wholesale, then delete POSTGRES_PASSWORD, VECTORDB_PASSWORD, APP_SECRET, UMAMI_DB_USER and UMAMI_DB_PASSWORD from the copy. By subtraction rather than by retyping: an include-list quietly drops whatever else the host holds — Admin__Emails (a lockout, the admin console being OAuth-only), OAuth__*, Ai__*, Knowledge__IngestApiKey. The five below are the required floor, not the whole file:

    Terminal window
    cat > /opt/pia/.env.prod.server << 'EOF'
    ASPNETCORE_ENVIRONMENT=Production
    Database__Provider=postgresql
    Database__ConnectionString=Host=postgres;Database=pia;Username=pia;Password=<db-owner-password>
    Jwt__SecretKey=<existing value from .env.prod>
    Encryption__MasterKey=<existing value from .env.prod>
    Knowledge__ConnectionString=Host=pia-vectordb;Database=pia_knowledge;Username=pia;Password=<vectordb-password>
    EOF
    chmod 600 /opt/pia/.env.prod.server

    Reuse the existing Encryption__MasterKey verbatim. A new one makes every encrypted record unreadable. Leave .env.prod untouched for now — step 7 trims it, and only after the new compose file is running.

  3. Add TEMPORAL_DB_PASSWORD to .env.prod.

    Terminal window
    openssl rand -base64 24 # then add TEMPORAL_DB_PASSWORD=<value> to /opt/pia/.env.prod

    This key has no fallback in the compose file. Unset, temporal-db-init creates the Temporal role with an empty password and the Temporal server never connects.

  4. Get the new files onto the host. Merging to master does this for you — the workflow copies all four. By hand:

    Terminal window
    scp docker-compose.prod.yml temporal-dynamicconfig.yaml root@<server-ip>:/opt/pia/

    Caddyfile and init-db.sh are unchanged and already there. temporal-dynamicconfig.yaml is new and mandatory: it is bind-mounted as a file, so if it is missing Docker creates a directory in its place and the Temporal server exits on unable to create dynamic config before opening a port.

  5. Check the Postgres mount path. The compose file points postgres at /mnt/HC_Volume_104717731/postgres. If that is not this host’s mount, edit the line before starting — Docker would otherwise create the path on the boot disk and Postgres would initialize an empty cluster there, next to your untouched data.

  6. Deploy.

    Terminal window
    cd /opt/pia
    docker compose --env-file .env.prod -f docker-compose.prod.yml pull
    docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --force-recreate
  7. Verify, then trim .env.prod.

    Terminal window
    docker compose --env-file .env.prod -f docker-compose.prod.yml ps -a
    docker compose --env-file .env.prod -f docker-compose.prod.yml logs pia-migrate
    curl https://cloud.pia.de/health

    Expect pia-migrate, temporal-db-init, temporal-schema-init and temporal-namespace at Exited (0) — that is their finished state, not a failure, and they stay in docker ps -a from here on. Everything else should be up, with temporal healthy.

    Once /health returns 200, delete the application keys from .env.prod, leaving the infrastructure ones. postgres and umami read nothing else from that file, so nothing needs recreating.

  8. Optional: turn on the operator runtime. The Temporal services run either way, but the worker stays off until you add four keys to .env.prod.server — see the operator runtime.

Restore .env.prod from .env.prod.backup, put the previous docker-compose.prod.yml back, and start it against the digest recorded in step 1. Schema migrations are not reverted, and this jump is not purely additive: DropGuardrailContentPreview removes guardrail_decisions.ContentPreview, which the older image still selects, so its guardrail reads fail until the dump from step 1 is restored. The rest are additive and nullable and the old server ignores those columns. Either way, a rollback that has already served traffic is better resolved forward. .env.prod.server can stay on disk; the old compose file ignores it.

A separate, later jump, and one that applies to every host rather than only pre-pia-migrate ones. The AddCreditAccounting migration changes the unit group limits are denominated in, and it converts what is already there — no operator action is required, but two things move under you:

  • Every group limit is divided by 1,000 and rounded up. A group on 1000000 weekly tokens comes out at 1000 weekly credits, which is the same traffic at the default provider rate. A group whose settings carried no limits at all stays unlimited, exactly as before. The sliding pool multiplier is seeded to 0 everywhere, so no group starts pooling until someone turns it on.
  • The shared pool is spent before anyone’s own allowance. Switching the multiplier on means the group’s carried-over credits go first and members’ own weekly counters do not move until the pool is empty. One consequence to price in: because pool draws do not count as own use, a group whose weekly spend stays inside its pool renews the same pool every week instead of burning it down. It never grows, and the per-week total stays inside the baseline.
  • The weekly counter switches from a rolling seven days to a calendar week, anchored at Monday 00:00 in TokenUsage__WeekStartTimeZone (default Europe/Berlin). This is the boundary the weekly 429 always claimed. On the first Monday after deploy, every member with weekly spend behind them gets it back at once rather than a day at a time — plan the deploy for a Monday if that matters to you.

Historic usage rows are re-priced at the anchor rate using the cache-hit multiplier each row was charged at, so the past reads the same as it did. Rate cards on existing providers start at the anchor, and a provider that carried a cache-hit multiplier keeps its discount: the multiplier was a fraction of an input token, so its cached input rate becomes that fraction of the input rate. Nothing needs re-entering.