Deployment¶
PrivaCI ships as a container image, a local evaluation compose.yml (Docker or
Podman), and a Helm chart for scheduled Kubernetes runs. All artifacts target a
batch CLI model: the container runs privaci run, streams masked rows, and
exits — it is not a long-lived daemon.
Container image¶
| Property | Value |
|---|---|
| Base | python:3.12-slim-bookworm (digest-pinned on release tags) |
| User | privaci (UID/GID 10001) |
| Entrypoint | privaci |
| SpaCy | en_core_web_sm baked in at build time |
| Size budget | ≤ 600 MB uncompressed |
| Filesystem | Read-only root compatible; runtime writes only to /tmp |
Build locally¶
docker build -t ghcr.io/boundarylogic/privaci:local .
scripts/verify-image.sh ghcr.io/boundarylogic/privaci:local
Run read-only¶
docker run --rm --read-only --tmpfs /tmp \
-e SOURCE_DB_URL=postgresql://... \
-e TARGET_DB_URL=postgresql://... \
-e ANONYMIZATION_SALT="$(privaci gen-salt)" \
-v "$(pwd)/mask-rules.yaml:/config/mask-rules.yaml:ro" \
ghcr.io/boundarylogic/privaci:local run --config /config/mask-rules.yaml
Evaluation compose (compose.yml)¶
Brings up source Postgres (synthetic seed), empty target Postgres, and the PrivaCI engine. Evaluation only — not for production.
export ANONYMIZATION_SALT="$(privaci gen-salt)"
make eval-up # auto-detects Docker or Podman; tear down with: make eval-down
make eval-up wraps scripts/eval-stack.sh, which picks an available compose
engine for you. To drive compose directly instead:
docker compose up --build # Docker
podman compose up --build # Podman (Compose v2 plugin)
podman-compose up --build # Podman (standalone Python, >= 1.0)
- Source seed:
deploy/demo-seed/(~500 synthetic users,example.testemails). - Config:
examples/mask-rules.demo.yaml(masksusers, copiesorganizationsverbatim so itsNOT NULLFK referents stay valid). - Engine runs with
read_only: trueandtmpfson/tmp. - stdout emits JSON-lines events including
run.endwithstatus: succeeded.
For long-running dev databases, use compose.dev.yml instead (see
local-development.md).
Docker vs Podman¶
The image, Helm chart, and release workflow are engine-independent. Only the evaluation compose stack touches the host, and it is verified on both engines. Windows users run any of these through Docker Desktop or WSL2 — see Windows below.
| Engine | Command | Notes |
|---|---|---|
| Docker | docker compose up --build |
Requires a reachable daemon and the user in the docker group. |
| Podman (plugin) | podman compose up --build |
Delegates to the Docker Compose v2 plugin when installed. |
| Podman (Python) | podman-compose up --build |
Standalone; >= 1.0 supports depends_on: condition: service_healthy. |
Key portability details, all already handled in compose.yml:
- SELinux. Bind mounts carry the
:zlabel, so repo files (config + seed) are readable from the container on enforcing hosts (Fedora, RHEL, CentOS Stream, Rocky, Alma).:zis a no-op on non-SELinux Docker. - Rootless Podman. The engine runs as UID 10001 with a read-only root and a
/tmptmpfs, which maps cleanly through Podman's user namespace. docker.sockpermission denied is a host setup issue, not a PrivaCI one: add your user to thedockergroup (sudo usermod -aG docker $USER, then re-login) or use Podman.scripts/eval-stack.shfalls back to Podman automatically when the Docker daemon is unreachable.
The verification script accepts either engine via CONTAINER_ENGINE:
CONTAINER_ENGINE=podman scripts/verify-image.sh ghcr.io/boundarylogic/privaci:local
Windows¶
PrivaCI is a Linux container, so on Windows it runs through a Linux backend. Docker Desktop with the WSL2 backend is the recommended setup; plain WSL2 (Ubuntu) gives the same experience as a native Linux host. The image, the pre-built GHCR container, and the Helm chart all work unchanged — only the shell-based convenience wrappers differ.
| Approach | Works | How |
|---|---|---|
| WSL2 (recommended) | Yes | Run all Linux commands as-is, including make eval-up and scripts/eval-stack.sh. |
| Docker Desktop + PowerShell | Yes | Use docker compose directly; make and the .sh helpers need Git Bash or WSL. |
| Helm / Kubernetes | Yes | helm install is OS-independent. |
| Native Windows (no Linux backend) | No | The Linux image cannot run without WSL2 or a VM — this is standard for Docker on Windows. |
From PowerShell with Docker Desktop running, set the salt with PowerShell syntax
(not export) and drive compose directly:
$env:ANONYMIZATION_SALT = "$(privaci gen-salt)" # or paste a 64-hex-char value
docker compose up --build
The :z SELinux labels on the bind mounts are ignored by Docker Desktop, just
as they are on macOS, so the same compose.yml works without edits.
Helm chart¶
Chart path: deploy/helm/privaci/
# Lint locally (helm required)
scripts/lint-helm.sh
# Install — credentials live in existing Secrets, never inline
helm install vp deploy/helm/privaci \
--set secrets.sourceDbUrlSecret=my-source-creds \
--set secrets.targetDbUrlSecret=my-target-creds \
--set secrets.saltSecret=my-salt
Defaults¶
| Resource | Purpose |
|---|---|
CronJob |
Scheduled masking run (schedule in values.yaml) |
ConfigMap |
mask-rules.yaml |
Secret refs |
SOURCE_DB_URL, TARGET_DB_URL, ANONYMIZATION_SALT |
securityContext |
runAsNonRoot, readOnlyRootFilesystem, dropped caps |
Override values.yaml for image tag, resources, affinity, tolerations, and
extraEnv hooks for the commercial layer.
Release channels¶
PrivaCI publishes two channels (ADR-0007). The commercial layer pins the engine
by CONTRACT_VERSION (privaci --contract-version, currently 1.0) and
promotes only after its integration suite passes against a stable engine
release.
| Channel | Git tag | GitHub release | Container tags | Audience |
|---|---|---|---|---|
| Beta | vX.Y.Z-beta.N |
Pre-release | :X.Y.Z-beta.N, :beta, :edge |
Early adopters, CI smoke on main |
| Stable | vX.Y.Z |
Latest release | :X.Y.Z, :X.Y, :latest |
Production and Marketplace image builds |
Rules:
- Never build the official Marketplace image from
mainor a beta tag — stable tags only. - Beta tags are cut from
mainfor fast iteration; stable tags follow after beta soak and full gate green. - OCI images carry
org.opencontainers.image.versionandio.boundarylogic.contract_versionlabels for machine pinning.
Pin a consumer to a release¶
# Contract ABI (commercial layer compatibility)
privaci --contract-version
# Image by channel
docker pull ghcr.io/boundarylogic/privaci:0.1.0-beta.1 # exact beta
docker pull ghcr.io/boundarylogic/privaci:beta # rolling beta head
docker pull ghcr.io/boundarylogic/privaci:1.0.0 # exact stable (future)
docker pull ghcr.io/boundarylogic/privaci:latest # stable head only
Release publishing¶
Tag pushes (v*) trigger
.github/workflows/release.yml:
- Contract-version and pack-key guards before build
- Multi-arch image (
linux/amd64,linux/arm64) →ghcr.io/boundarylogic/privaci - Channel-aware tags (see table above)
- SPDX SBOM via
syft - Image signing via
cosign(keyless OIDC where available) - Helm chart packaged to
oci://ghcr.io/boundarylogic/charts - GitHub Release (pre-release for beta tags)
GHCR publish credentials¶
The BoundaryLogic org rejects GITHUB_TOKEN package writes (permission_denied:
write_package) even when the container package lists this repository under
Manage Actions access. The release workflow therefore authenticates to GHCR
with a classic personal access token stored as a repository secret.
- As a GitHub user with write access to org packages, create a classic
PAT with scopes
write:packagesandread:packages. - In this repo: Settings → Secrets and variables → Actions → New repository
secret → name
GHCR_TOKEN, paste the PAT. - If the PAT owner is not the user who pushes release tags, also add
GHCR_USERNAME(the PAT owner's GitHub login).
Re-run the failed Release workflow job after adding the secret.
See release-infrastructure runbook for the full checklist (environments, PyPI Trusted Publishing, tag re-run caveats).
Cut a beta release¶
# 1. Ensure CHANGELOG [Unreleased] is current and gates are green.
# 2. Provision pack signing keys (one-time per rotation):
python scripts/generate_pack_signing_key.py
# Store PRIVATE_KEY_HEX in GitHub Actions secrets (PACK_SIGNING_PRIVATE_KEY).
# Publish PUBLIC_KEY_HEX in the release notes; operators set PRIVACI_PACK_PUBLIC_KEY.
git tag -a v0.1.0-beta.1 -m "v0.1.0-beta.1"
git push origin v0.1.0-beta.1
See also pack-signing runbook and git-history privacy runbook.