Aller au contenu

Upgrading an existing host

Ce contenu n’est pas encore disponible dans votre langue.

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.

A later jump for every host with E2EE. AddWrappedUmkApprovalSignature adds three nullable columns to wrapped_umks and needs no operator action. From here on the server refuses a device approval without a valid signature of the approving device, and hands that signature back with the wrapped key.

Update the server before the desktop clients. Newer desktop clients accept a key handed over by another device only with that signature, and an older server returns none, so adding a device fails until the server is updated. Clients that do not check the signature yet work against both.

Server 1.4: retention, AI feedback and recovery

Section titled “Server 1.4: retention, AI feedback and recovery”

Every host. The migrate job adds four columns and one table, drops the unused password_reset_tokens table and converts stored MFA recovery codes in place. What moves under you:

  • Retention periods get shorter and cannot be switched off. Audit log 180 days, sign-ins 90, IP addresses 14, guardrail decisions 30, token and embedding usage 90. A configured value above those is capped, and 0 now gets the default instead of keeping rows forever. The first sweep after the upgrade deletes what is older — take a database dump first if you need the history. See Retention.
  • Deleted items go for good. Soft-deleted synced items and knowledge-base documents are removed 30 days after the delete, sync events after 90 days, license events after 180, and a device’s sync cursor 180 days after its last push. A deleted account is purged 14 days after the delete, at most 30; a negative Gdpr__DeletionGraceDays no longer switches purging off. Refresh tokens live at most 30 days and an operator assignment’s plaintext at most 72 hours.
  • Logs name accounts, not addresses. Log lines carry the account id instead of an e-mail address, and a client’s network (IPv4 /24, IPv6 /64) instead of its IP address. Anything that searched the container logs for an address has to search for the account id instead.
  • AI feedback reports lose their text 30 days after they are closed and 90 days after they arrived at the latest; Feedback__AnswerTextRetentionDays is gone, replaced by Feedback__ClosedContentRetentionDays and Feedback__ContentRetentionDays. Feedback__PrivacyNotifyAddress is new. See AI feedback.
  • Legal links. Set Legal__ImprintUrl and Legal__PrivacyUrl to link your imprint and privacy notice from the registration and password pages.
  • Update the server before the desktop clients. Pia Desktop 1.4.x uploads the recovery proof key with a call an older server does not have; it skips the step there, but the protection starts only once the server is updated. Older desktop clients keep working against the new server.