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.
What changes
Section titled “What changes”| 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 |
-
Back up.
Terminal window cd /opt/piacp .env.prod .env.prod.backupdocker inspect --format '{{index .RepoDigests 0}}' ghcr.io/pia-ai-dev/pia-server:latestdocker compose -f docker-compose.prod.yml exec postgres pg_dumpall -U pia > /root/pia-preupgrade.sqlKeep the digest. It is what a rollback pins to, since the tag is
:latest. -
Create
.env.prod.server. Copy.env.prodwholesale, then deletePOSTGRES_PASSWORD,VECTORDB_PASSWORD,APP_SECRET,UMAMI_DB_USERandUMAMI_DB_PASSWORDfrom 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=ProductionDatabase__Provider=postgresqlDatabase__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>EOFchmod 600 /opt/pia/.env.prod.serverReuse the existing
Encryption__MasterKeyverbatim. A new one makes every encrypted record unreadable. Leave.env.produntouched for now — step 7 trims it, and only after the new compose file is running. -
Add
TEMPORAL_DB_PASSWORDto.env.prod.Terminal window openssl rand -base64 24 # then add TEMPORAL_DB_PASSWORD=<value> to /opt/pia/.env.prodThis key has no fallback in the compose file. Unset,
temporal-db-initcreates the Temporal role with an empty password and the Temporal server never connects. -
Get the new files onto the host. Merging to
masterdoes 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/Caddyfileandinit-db.share unchanged and already there.temporal-dynamicconfig.yamlis 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 onunable to create dynamic configbefore opening a port. -
Check the Postgres mount path. The compose file points
postgresat/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. -
Deploy.
Terminal window cd /opt/piadocker compose --env-file .env.prod -f docker-compose.prod.yml pulldocker compose --env-file .env.prod -f docker-compose.prod.yml up -d --force-recreate -
Verify, then trim
.env.prod.Terminal window docker compose --env-file .env.prod -f docker-compose.prod.yml ps -adocker compose --env-file .env.prod -f docker-compose.prod.yml logs pia-migratecurl https://cloud.pia.de/healthExpect
pia-migrate,temporal-db-init,temporal-schema-initandtemporal-namespaceatExited (0)— that is their finished state, not a failure, and they stay indocker ps -afrom here on. Everything else should be up, withtemporalhealthy.Once
/healthreturns 200, delete the application keys from.env.prod, leaving the infrastructure ones.postgresandumamiread nothing else from that file, so nothing needs recreating. -
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.
Rolling back
Section titled “Rolling back”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.
Tokens become credits
Section titled “Tokens become credits”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
1000000weekly tokens comes out at1000weekly 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 to0everywhere, 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(defaultEurope/Berlin). This is the boundary the weekly429always 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.
Device approvals are signed
Section titled “Device approvals are signed”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
0now 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__DeletionGraceDaysno 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__AnswerTextRetentionDaysis gone, replaced byFeedback__ClosedContentRetentionDaysandFeedback__ContentRetentionDays.Feedback__PrivacyNotifyAddressis new. See AI feedback. - Legal links. Set
Legal__ImprintUrlandLegal__PrivacyUrlto 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.
- Production deployment — the full reference for a fresh host, including the database role split
- Configuration — every key both environment files accept