Skip to content

# Provisa Deployment Plan

## Overview

Provisa ships as three distinct packages per platform. (REQ-630) The split is driven by GitHub Actions' 2 GB artifact limit — container image tarballs alone exceed it. (REQ-630) The three packages map directly to the three docker-compose layers: (REQ-630)

Package | Services | docker-compose file
--- | --- | ---
Core | postgres, pgbouncer, redis, minio, trino, zaychik + Python backend + UI | `docker-compose.core.yml`
Observability (Obs) | otlp2parquet, otel-collector, prometheus, tempo, grafana | `docker-compose.observability.yml`
Demo | petstore-mock, graphql-demo | `docker-compose.demo.yml`

Dependency chain: Core ← Obs ← Demo (demo requires obs; obs requires core). (REQ-631)

## Platform Matrix

Package | macOS | Windows | Linux
--- | --- | --- | ---
Core | DMG (Lima + nerdctl) | NSIS .exe (VirtualBox OVA) | AppImage (rootless Docker)
Obs | DMG (image load into Lima) | NSIS .exe (image load into VirtualBox VM) | bundled into Core AppImage
Demo | DMG (image load into Lima) | NSIS .exe (image load into VirtualBox VM) | not included

Linux rationale: Linux users are typically server/technical installs. (REQ-227) OTel observability is useful in production; petstore/graphql demo services are not. One self-contained AppImage is simpler to distribute. (REQ-632)

## Extension Model (macOS + Windows)

Core is the only installer that creates the VM runtime (Lima / VirtualBox). (REQ-633) Obs and Demo are extension packages — they load images into the existing VM and drop a compose file into a well-known extensions directory. (REQ-633) Core's launcher detects installed extensions at startup and composes the service set dynamically. (REQ-633)

### Extension directory

Platform | Path
--- | ---
macOS | `~/.provisa/extensions/`
Windows | `%USERPROFILE%\.provisa\extensions\`

Each extension drops:

    extensions/
      observability/
        docker-compose.observability.yml
        (obs-specific configs already in core's ~/.provisa/observability/)
      demo/
        docker-compose.demo.yml
        (demo server source already in core's ~/.provisa/demo/)

### Compose file assembly at launch

The launcher builds the compose file list dynamically: (REQ-633)

    core:  docker-compose.core.yml + docker-compose.app.yml + docker-compose.airgap.yml
    + obs:  + extensions/observability/docker-compose.observability.yml
    + demo: + extensions/demo/docker-compose.demo.yml

When an extension is installed, the launcher restarts all services together with the expanded file list. (REQ-633) Trino picks up the OTel `JAVA_TOOL_OPTIONS` override from `docker-compose.observability.yml` on that restart. (REQ-633)

## macOS Packages

### Core DMG (`Provisa-<version>.dmg`)

`packaging/macos/build-dmg.sh` builds the Core DMG (Core package only).

Contents of DMG: - `Provisa.app` — signed + notarized SwiftUI launcher (ProvisaLauncher) (REQ-227) - `images/` — core image tarballs (hidden from Finder): - `python-3.12-slim.tar.gz` - `postgres-16.tar.gz` - `pgbouncer-latest.tar.gz` - `redis-7-alpine.tar.gz` - `minio-latest.tar.gz` - `trino-480.tar.gz` - `zaychik-local.tar.gz` - `nerdctl/` — `nerdctl-full-2.2.2-linux-arm64.tar.gz` (hidden) (REQ-228) - `vm-image/` — `provisa-vm.img` Ubuntu 24.04 arm64 (hidden) (REQ-228)

`Provisa.app/Contents/Resources/` embeds: (REQ-294) - `docker-compose.core.yml`, `docker-compose.app.yml`, `docker-compose.airgap.yml` - `config/`, `db/`, `trino/`, `observability/` (trino-otel dir + OTel Java agent jar) - `provisa-source/` (Dockerfile, main.py, pyproject.toml, provisa/, static UI, wheels)

`first-launch.sh`: - Copies `observability/` configs but does not start obs services (no obs images yet) - Copies `demo/` source but does not start demo services

### Obs DMG (`Provisa-Obs-<version>.dmg`)

Contents: - `install-obs.sh` — installer script (no `.app`, just a shell script run via a minimal DMG or a signed pkg) - `images/` (hidden): - `otlp2parquet-latest.tar.gz` - `otel-collector-contrib-0.99.0.tar.gz` - `prometheus-v2.51.2.tar.gz` - `tempo-2.4.1.tar.gz` - `grafana-10.4.2.tar.gz`

`install-obs.sh` steps: 1. Check Lima VM `provisa` exists (core must be installed). (REQ-633) 2. Start Lima VM if not running. (REQ-228) 3. `limactl shell provisa sudo ctr images import` for each image tarball. (REQ-294) 4. Write `~/.provisa/extensions/observability/docker-compose.observability.yml`. (REQ-633) 5. Print: "Observability installed. Restart Provisa to activate."

Build script: `packaging/macos/build-dmg-obs.sh` - Pulls + saves obs images (`--platform linux/arm64`, gzip compressed) (REQ-294) - Embeds `install-obs.sh` + images into a minimal DMG - Signs + notarizes `install-obs.sh` (REQ-227)

### Demo DMG (`Provisa-Demo-<version>.dmg`)

Requires Obs to be installed. (REQ-631)

Contents: - `install-demo.sh` - `images/` (hidden): - `petstore3-unstable.tar.gz` - `graphql-demo-local.tar.gz`

`install-demo.sh` steps: 1. Check `~/.provisa/extensions/observability/` exists (obs must be installed). (REQ-631) 2. Start Lima VM if not running. (REQ-228) 3. Import demo image tarballs into Lima. (REQ-294) 4. Write `~/.provisa/extensions/demo/docker-compose.demo.yml`. (REQ-633) 5. Print: "Demo installed. Restart Provisa to activate."

Build script: `packaging/macos/build-dmg-demo.sh`

### ProvisaLauncher changes (`ServiceStatus.swift` / `ScriptRunner.swift`)

The launcher's `provisa start` path:

1. Enumerates `~/.provisa/extensions/*/docker-compose.*.yml` at startup. (REQ-633)
2. Appends each found file to the compose file list. (REQ-633)
3. Sets `PROVISA_REDIRECT_ENABLED`, MinIO, and OTel env vars only when the obs extension is present. (REQ-633)

## Windows Packages

### Core Installer — native tier (`Provisa-Setup-<version>.exe`) (REQ-979)

`packaging/windows/build-sfx.ps1` builds the Core installer with Inno Setup. It bundles a standalone Python runtime (python-build-standalone for x86_64-pc-windows-msvc) with the provisa wheel + uvicorn + duckdb/pg_duckdb + aiosqlite, and stages the built UI at `<site-packages>\static`. The base installer ships no Docker, no VM, and no container images — no VirtualBox, no OVA, no Trino.

`first-launch-native.ps1` stages the runtime to `%USERPROFILE%\.provisa\runtime` and writes config; `provisa-native.ps1` runs the two uvicorn processes (API factory + ui_server). Mirrors macOS `bundle_native_runtime`.

### Container Tier — on-demand upgrade (`Provisa-Container-Setup-<version>.exe`) (REQ-889, REQ-633)

`packaging/windows/build-container.ps1` builds a separate installer (Inno Setup) that adds the compute stack (Trino + services) via WSL2 + containerd — the Windows equivalent of the macOS Lima tier. VirtualBox is never used. It bundles:

- the compose tree (core/app/airgap/observability/demo, config, db, trino config minus plugins),
- the core image tarballs (`docker-images-core-amd64` from CI),
- `nerdctl-full-<ver>-linux-amd64.tar.gz` (version-matched to the macOS Lima tier),
- a WSL base rootfs (`rootfs.tar.gz`).

`install-container.ps1` steps:

1. Enable WSL2 (`wsl --install --no-distribution`, `--set-default-version 2`).
2. `wsl --import provisa %USERPROFILE%\.provisa\wsl\provisa rootfs.tar.gz --version 2`.
3. `wsl/provision-containerd.sh` installs nerdctl-full; `wsl/start-containerd.sh` starts containerd (no systemd in WSL2 by default).
4. `nerdctl load` each core image tarball.
5. Copy the compose tree to `/opt/provisa/compose` inside the distro.
6. Write config `runtime: container`, stop the native tier, start the stack.

`provisa-container.ps1` routes compose through `wsl -d provisa -u root sh -c 'cd /opt/provisa/compose && nerdctl compose -f ... <cmd>'`, mirroring the `RUNTIME=lima` routing in `scripts/provisa`. WSL2 forwards `localhost` ports, so the UI/API are reachable at `http://localhost:3000`/`:8000`. The tier is additive and reversible: switch back to the native tier with `provisa-native.ps1`; `uninstall.ps1` unregisters the WSL distro.

### Obs Installer (`Provisa-Obs-Setup-<version>.exe`) — container tier

`install-obs.ps1` steps: 1. Check the container-tier runtime exists and is running. (REQ-633) 2. `docker load` each obs image tarball into the runtime. (REQ-633) 3. Write `%USERPROFILE%\.provisa\extensions\observability\docker-compose.observability.yml`. (REQ-633) 4. Prompt user to restart Provisa.

Build script: `packaging/windows/build-installer-obs.ps1`

### Demo Installer (`Provisa-Demo-Setup-<version>.exe`)

Requires Obs installer. (REQ-631)

Same pattern as obs — loads demo images, writes extension compose file. (REQ-633)

Build script: `packaging/windows/build-installer-demo.ps1`

### Container-tier CLI extension detection

The container-tier CLI uses the same extension detection as ProvisaLauncher: enumerate `$env:USERPROFILE\.provisa\extensions\*/docker-compose.*.yml` and append to the compose file list. (REQ-633)

## Linux AppImage

`packaging/linux/build-appimage.sh` bundles core images (postgres, pgbouncer, minio, redis, trino, zaychik) plus obs images. MinIO is a core service (REQ-561) and is bundled with the core image set. No demo. (REQ-632)

### `save_images()` target list

    # Core
    "postgres:16"
    "edoburu/pgbouncer:latest"
    "redis:7-alpine"
    "minio/minio:latest"
    "trinodb/trino:480"
    "provisa/zaychik:local"   # built from source

    # Obs (bundled directly — no separate download on Linux)
    "ghcr.io/smithclay/otlp2parquet:latest"
    "otel/opentelemetry-collector-contrib:0.99.0"
    "prom/prometheus:v2.51.2"
    "grafana/tempo:2.4.1"
    "grafana/grafana:10.4.2"

### `build_appdir()`

- Copies `docker-compose.core.yml` + `docker-compose.observability.yml` into `${APPDIR}/compose/`
- `AppRun` / `first-launch.sh` always starts core + obs (no flag needed) (REQ-632)
- No demo compose file in the bundle (REQ-632)

### `first-launch.sh` (Linux)

Start command:

    docker compose \
      -f compose/docker-compose.core.yml \
      -f compose/docker-compose.observability.yml \
      -f compose/docker-compose.app.yml \
      -f compose/docker-compose.airgap.yml \
      up -d

## CI / GitHub Actions

Three parallel build jobs per platform. (REQ-630) Each uploads its artifact separately, staying under the 2 GB GitHub artifact limit. (REQ-630)

    jobs:
      build-macos-core:
        outputs: Provisa-<version>.dmg

      build-macos-obs:
        outputs: Provisa-Obs-<version>.dmg

      build-macos-demo:
        outputs: Provisa-Demo-<version>.dmg

      build-windows-core:
        outputs: Provisa-Setup-<version>.exe

      build-windows-obs:
        outputs: Provisa-Obs-Setup-<version>.exe

      build-windows-demo:
        outputs: Provisa-Demo-Setup-<version>.exe

      build-linux:
        outputs: Provisa-<version>.AppImage   # core + obs, no demo

All jobs are independent and run in parallel. (REQ-630) Demo jobs have a logical dependency on obs (checked at install time by the installer script, not enforced by CI). (REQ-631)

## Dev Environment

The dev environment mirrors the packaged product's compose layers but with the Python backend and UI running on the host (uvicorn + vite), not in containers. (REQ-634) This means `docker-compose.app.yml` is never used in dev — it would bind ports 8000 and 3000 to containerized services, conflicting with the local processes. (REQ-634)

### Compose stacks

Mode | Compose files used
--- | ---
Core only | `core.yml` + `dev-install.yml`
Core + Obs | `core.yml` + `dev-install.yml` + `observability.yml`
Core + Obs + Demo | `core.yml` + `dev-install.yml` + `observability.yml` + `demo.yml`

`docker-compose.app.yml` and `docker-compose.airgap.yml` are packaged-product only — never included in dev. (REQ-634)

### Port map

All service ports are exposed to the host by `dev-install.yml` (core services) or `observability.yml` (obs services). The local backend connects to everything via `localhost`. (REQ-634)

Port | Service | Who binds it
--- | --- | ---
5432 | postgres | `dev-install.yml`
6432 | pgbouncer | `dev-install.yml`
6379 | redis | `dev-install.yml`
8080 | trino | `dev-install.yml`
8480 | zaychik (Flight SQL proxy) | `dev-install.yml`
9000 | minio S3 | `dev-install.yml`
9001 | minio console | `dev-install.yml`
4317 | otel-collector gRPC | `observability.yml`
4318 | otel-collector HTTP | `observability.yml`
4319 | otlp2parquet HTTP | `observability.yml`
9090 | prometheus | `observability.yml`
3100 | grafana | `observability.yml`
18080 | petstore-mock | `demo.yml`
4000 | graphql-demo | `demo.yml`
8000 | Python backend (uvicorn) | host process — never containerised in dev
3000 | UI (vite dev server) | host process — never containerised in dev

Ports 8000 and 3000 must never appear in any dev compose file. (REQ-634) Any future compose overlay that adds a service binding those ports would silently break the dev environment.

### Backend OTel endpoint in dev

`docker-compose.app.yml` points the containerised backend to `http://otel-collector:4317` (Docker-internal gRPC). The local backend cannot reach that hostname. (REQ-634)

When obs is active in dev, `start-ui-install.sh` sets: (REQ-330)

    OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4319"   # OTLP HTTP to otlp2parquet, host-exposed
    OTEL_SERVICE_NAME="provisa"

When obs is not active, these vars are unset (spans are dropped). (REQ-330)

### `start-ui-install.sh` modes

    ./start-ui-install.sh              # core only
    ./start-ui-install.sh --demo       # core + obs + demo  (--demo always implies obs)

There is intentionally no `--obs` flag without demo — in dev, running obs without demo data produces an empty Grafana/Tempo dashboard, which is not useful. (REQ-634) The flag may be added later if needed.

## Shipped Components

The three-package split, the extension/detection model, the per-OS installers, and the parallel CI jobs are shipped. (REQ-630, REQ-631, REQ-632, REQ-633)

- `docker-compose.observability.yml` is self-contained.
- `docker-compose.dev-install.yml` binds core service ports (including minio).
- `start-ui-install.sh` does dynamic compose assembly with demo-conditional env vars, and sets `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4319` + `OTEL_SERVICE_NAME` on the backend env when demo is active.
- `build-dmg.sh` builds the Core DMG; obs/demo configs stay in Resources.
- `build-dmg-obs.sh` pulls obs images and builds the obs DMG.
- `build-dmg-demo.sh` pulls demo images and builds the demo DMG.
- ProvisaLauncher does extension detection in `ServiceStatus.swift` / compose assembly.
- `first-launch.sh` (macOS) copies obs/demo configs without starting their services.
- `build-sfx.ps1` builds the native Core installer (embedded Python, no images).
- `build-installer-obs.ps1` builds the Windows obs installer (container tier).
- `build-installer-demo.ps1` builds the Windows demo installer (container tier).
- `provisa-native.ps1` runs the native tier; the container-tier CLI does extension detection.
- `build-appimage.sh` bundles core + obs images with always-on obs compose, no demo.
- CI workflow runs three parallel build jobs per platform.