planchi

Self-host installation

Run planchi in your own infrastructure as a Docker Compose stack. Delivery consists of versioned container images and Compose configuration — not source code. To go live you need the artefacts from your agreement: the Compose package, a signed license file, and credentials for the private container registry.

Requirements

  • Docker Engine and Docker Compose v2 (on Windows: Docker Desktop)
  • Outbound HTTPS to the container registry (ghcr.io) for image pulls
  • Reverse proxy or ingress with TLS termination and WebSocket support
  • Provided by the vendor: Compose package, license.json, read-only registry credentials

CPU, RAM and storage

Guidelines for app, PostgreSQL, PlantUML and Seq on one host. Load depends on concurrent users and data volume.

Minimum

2 vCPU · 5 GB RAM · 40 GB SSD

Small teams / pilot (about up to 10 active users), including Seq.

Note What drives sizing?

The stack includes the planchi app (ASP.NET / Blazor), PostgreSQL, PlantUML and Seq (structured logs, about +1 GB RAM). No GPU is required. Language models for MCP run on the AI client or provider, not on this host. Disk grows with project documents, wiki media, Seq retention and backups — SSD recommended.

Installation

  1. Unpack the Compose package (directory containing docker-compose.yml). Run every docker compose command from that folder afterwards.
  2. Create the environment from .env.example and set at least POSTGRES_PASSWORD, PUBLIC_BASE_URL, SUPER_ADMIN_EMAIL, and SEQ_ADMIN_PASSWORD. Change ports if 8080/5432/5341 are already in use.
    cp .env.example .env
    # edit .env
    Note .env variables (reference)

    .env drives Compose and application configuration. Never commit secrets.

    IMAGE_REGISTRY / IMAGE_TAG
    Container image and version tag (e.g. 1.0.0 or latest).
    APP_PORT / POSTGRES_PORT / SEQ_PORT
    Host ports for the app, PostgreSQL and Seq UI.
    POSTGRES_*
    Database name, user, and password (defaults: plan_resuah / plan). Always set a strong POSTGRES_PASSWORD — used by the app and by backup.sh.
    PUBLIC_BASE_URL
    Public base URL exactly as in the browser (scheme, IP or hostname, port). Example: http://<your-ip>:<APP_PORT> — also used for the Entra redirect /signin-oidc.
    ALLOWED_HOSTS
    Allowed Host headers; * is common for internal deploys — set an explicit hostname in locked-down environments.
    SUPER_ADMIN_EMAIL
    Super-admin email (system overview, license, directory). Register that account locally once; break-glass /Account/Login?admin=true.
    LICENSE_DIR / LICENSING_REQUIRE
    Path to the license directory and whether a valid instance license is required (self-host: true).
    SEQ_ADMIN_PASSWORD / SEQ_SERVER_URL
    Password for the Seq UI (user: admin, applied on first start only). App’s internal Seq URL (default http://seq:80). Do not expose the Seq port publicly without VPN/auth.
    EMAIL_*
    Optional SMTP for invitations and notifications. Without SMTP, no mail is sent.
  3. Place the signed license file at ./license/license.json.
  4. Authenticate to the private registry, start the stack, and verify the health endpoint.
    echo "$TOKEN" | docker login ghcr.io -u USERNAME --password-stdin
    docker compose up -d
    curl -fsS http://<your-ip>:<APP_PORT>/health
    curl -fsS http://<your-ip>:<APP_PORT>/health/ready

    Command not in the package folder: “no configuration file provided”. If your user is not in the docker group: sudo docker …

  5. Point your reverse proxy at APP_PORT (default 8080). Database schema migrations run automatically on application start.
    Note What are migrations?

    Migrations are versioned schema changes to the PostgreSQL database (tables, columns, indexes). On startup the app checks the current schema and applies any missing migrations automatically. In practice: after image updates a container restart is usually enough — no manual SQL. Still take a database backup before larger version jumps.

  6. Open the app in the browser and register once with exactly SUPER_ADMIN_EMAIL (remember the password). No confirmation email — SMTP is not required for this. Enable Entra only afterwards, or you lock out break-glass access.

Updates

New releases are published as image tags in the registry. Optionally pin IMAGE_TAG in .env, then pull images and recreate containers. Take a database backup before major upgrades.

./update.sh
# or:
docker compose pull
docker compose up -d
Note Update flow

After the pull, the new app version starts. If the release includes schema changes, they are applied as migrations on startup. Rollback: set the previous IMAGE_TAG and pull/up again; restore from backup if needed.

Monitoring & logs

Structured app logs go to Seq by default. Availability checks use the health endpoints (e.g. Zabbix HTTP agent).

  • Seq UI: http://<your-ip>:<SEQ_PORT> — login user admin, password = SEQ_ADMIN_PASSWORD from .env. First login often requires a password change. Do not expose publicly without VPN/auth.
  • Alternatively raw logs: docker compose logs -f planchi-app (JSON on the console).
  • GET /health — liveness (process responds; Docker HEALTHCHECK).
  • GET /health/ready — readiness including PostgreSQL (for load balancers / monitoring).
  • Zabbix/Nagios etc.: HTTP check on /health/ready expecting status 200 and JSON status=healthy.
  • Own ELK/Splunk: remove the Seq service from Compose and clear Seq__ServerUrl / SEQ_SERVER_URL.
curl -fsS http://<your-ip>:<APP_PORT>/health
curl -fsS http://<your-ip>:<APP_PORT>/health/ready
Note What is checked?

/health only verifies that the app serves HTTP. /health/ready verifies the database connection — no secrets in the response. Postgres and PlantUML also have Compose healthchecks. Prometheus metrics are not included in this release.

Backup & restore

Recommended: PostgreSQL dump in custom format (-Fc). You set interval and retention (RPO/RTO).

  • Database: ./backup.sh or pg_dump (see command below)
  • Also: .env, license/, volume planchi-dataprotection-keys
  • Optional: volume planchi-seq-data (Seq log storage)

No cron in the package — example: daily via cron or Task Scheduler; frequency is your decision.

chmod +x backup.sh   # once
./backup.sh
# or manually:
mkdir -p backups
docker compose exec -T postgres \
  pg_dump -U plan -d plan_resuah -Fc > "backups/planchi-$(date +%F).dump"
Note Restore (short)

Stop the app, restore the dump, restore data-protection keys with the DB, start the app. Before major upgrades always take a fresh dump off-host.

docker compose stop planchi-app
cat backups/planchi-YYYY-MM-DD.dump | docker compose exec -T postgres \
  pg_restore -U plan -d plan_resuah --clean --if-exists
docker compose start planchi-app

License

The instance license is an offline-verifiable, cryptographically signed file (license.json) with a seat limit. If it expires or the seat cap is exceeded, existing operations remain available; adding further user seats is blocked. Status is shown as an in-app banner and under System overview (account configured via SUPER_ADMIN_EMAIL). Renewals or seat increases: info@resuah.ch

“Invalid signature” means the file is not signed with the key baked into the image (wrong sample file or a modified file). Use only the license.json issued by Resuah.

Microsoft Entra ID

Optional: one tenant for the whole instance (the company). planchi organizations are often departments. Coarse org roles come from Entra groups; project members and individual rights stay in planchi. The Owner role never comes from Entra. LDAP will follow the same model later.

Order (required)

  1. Super admin registered locally, at least one organization created in planchi. Entra does not create departments.
  2. Entra: app registration, single tenant. Platform Web. Copy the redirect URI from System overview → Directory: PUBLIC_BASE_URL + /signin-oidc, e.g. http://<your-ip>:<APP_PORT>/signin-oidc. Scheme, IP/hostname and port must match the browser address exactly.
  3. Create a client secret and note the secret value (not the secret ID).
  4. Token configuration: optional groups claim on the ID token. Assign groups to the enterprise app; users must be members.
  5. API permissions: application permission User.Read.All (not delegated), then Grant admin consent at the top of the same page — green check in the Status column.
  6. /admin/system → Directory: enable Entra, enter tenant, client, secret, map the group object ID to org + role (not Owner). Save.
  7. Test Microsoft sign-in with a user in the mapped group. Without at least one mapping, every login is rejected.

Which ID goes where

Tenant ID
App registration → Overview → Directory (tenant) ID.
Client ID
Application (client) ID. Not the app object ID.
Secret
The secret value from Certificates & secrets, not the secret ID. Rotate it if it appeared in chats.
Group object ID
Groups → open the group → Object ID. Not the client ID, not the app object ID.

When to restart

  • Tenant, client ID or secret changed: docker compose restart planchi-app.
  • Group mappings: take effect without a restart.
  • User.Read.All / admin consent or PUBLIC_BASE_URL: restart the app.

Who can get in

  • User in a mapped Entra group: Microsoft sign-in creates the account without email confirmation and adds org membership (directory role).
  • Org admin searches under Organization: tenant people only, immediate membership, no email link.
  • Invite an arbitrary email: not possible while Entra is on.

Several groups for the same org: highest coarse role wins. Group removed: only directory-managed membership drops; local memberships added via search stay. Project rights stay in planchi.

Note Where is admin consent?

Not next to User.Read.All — at the top of the table: “Grant admin consent for <org>”. The account needs Global Administrator or Privileged Role Administrator. Alternative: Enterprise applications → the same app → Permissions. Without the green check, member search returns 403.

Note Break-glass

The “Local administrator” link is hidden on the login page. Only PUBLIC_BASE_URL + /Account/Login?admin=true, e.g. http://<your-ip>:<APP_PORT>/Account/Login?admin=true — SUPER_ADMIN_EMAIL with the local password only. No SUPER_ADMIN_PASSWORD in .env. With Entra: no marketing pages, / goes to login, password and passkeys blocked for everyone else.

Note Typical errors
AADSTS50011
Redirect URI in Entra does not match the browser URL (http vs https, IP/hostname, APP_PORT). Copy the URI from Directory — do not register a different origin.
No Entra group mapped
Directory has no mapping row. Map the group object ID via ?admin=true — no restart.
No groups in the token
Add the groups claim on the ID token and assign groups to the enterprise app.
Groups in the token, no match
Check the group object ID, not the client ID.
Member search empty / 403
User.Read.All as an application permission plus admin consent, then restart the container.
no configuration file provided
cd into the folder with docker-compose.yml. Logs: docker compose logs -f planchi-app (sudo if needed). Seq often requires a password change on first login.

Reverse proxy

  • Terminate TLS at the proxy
  • Allow WebSockets for /_blazor
  • Do not cache /_framework (Blazor WebAssembly)
  • Set X-Forwarded-Proto and X-Forwarded-For (Compose enables FORWARDEDHEADERS)
  • Generous timeouts (WASM download, long requests)
Note Nginx excerpt

Terminate TLS at the proxy. On the host, point internally at 127.0.0.1 and APP_PORT from .env. The public URL remains http://<your-ip>:<APP_PORT> or https://<host> behind the proxy.

location / {
  proxy_pass http://127.0.0.1:<APP_PORT>;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_read_timeout 3600;
  proxy_buffering off;
}

location /_framework/ {
  proxy_pass http://127.0.0.1:<APP_PORT>;
  proxy_cache off;
  add_header Cache-Control "no-cache";
}

Source code is not part of the delivery. Registry access is read-only and may be revoked when the contract ends; an already running instance remains operational until the next image pull.

Next: Connect AI (MCP) →

planchi

Reconnecting…

One moment — we're reconnecting you.

Server unavailable

We'll keep trying automatically.

Server unavailable

We'll keep trying automatically.

Session paused

The server paused this session.

Couldn't resume session

Please try again or reload the page.

Update available

What's new

A new version of planchi has been released. Please update now — the app may not work correctly with outdated data in your browser. This dialog cannot be dismissed until you update.

All release notes

planchi

Connection interrupted

We'll reconnect automatically when the server is back.

Reload