Files
portfolio-hub/docs/container-operations.md
T

11 KiB

PortfolioHub v0.1.2 container contract

Publication is not deployment

Only a requester-authorized push of the strict tag v0.1.2 can publish this revision. The workflow checks that the tag equals [project].version, checks out github.sha, runs the complete Python suite, builds one linux/amd64 image from uv.lock, and tests those exact bytes before publication. It gives the same image exactly two selectors:

  • ghcr.io/fourblocks2020/portfolio-hub:<source-commit-first-12>
  • ghcr.io/fourblocks2020/portfolio-hub:v0.1.2

It never publishes latest. Both selectors must report the same registry digest; deployment and rollback must use the immutable identity:

ghcr.io/fourblocks2020/portfolio-hub@sha256:<64-lowercase-hex-digest>

OCI labels are fail-closed build inputs: org.opencontainers.image.source, the full 40-character org.opencontainers.image.revision, and strict org.opencontainers.image.version=v0.1.2. Publication does not deploy, replace, start, stop, or restart a production service. This repository provides no production Compose, Kubernetes, Helm, systemd, or Dockge configuration.

The Dockerfile frontend, Python base image, uv image, and GitHub Actions are digest/commit pinned. The workflow intentionally uses --provenance=false to preserve the tested docker save archive and build-once/no-rebuild identity. Registry-attached provenance, SBOM, signing, and attestations are a nonblocking v0.1.2 supply-chain gap; they must not be added by rebuilding a different image in the publish job.

Image runtime

  • platform: linux/amd64;
  • process: exec-form uvicorn app.main:app --host 0.0.0.0 --port 8000 as PID 1;
  • network: HTTP on fixed container port 8000 (configure only the host/service mapping, not the container port);
  • identity: numeric UID/GID 10001:10001;
  • filesystem: no application data path is written; the root filesystem may be read-only;
  • temporary space: a bounded /tmp tmpfs is safe but not required by normal application behavior;
  • shutdown: send SIGTERM, allow at least 10 seconds for Uvicorn graceful shutdown, then escalate only under the deployment platform's policy;
  • restart: application replicas may use the platform's normal failure restart policy; the one-shot migration must use restart no and must never loop after failure.

Recommended runtime restrictions are a read-only root filesystem, all capabilities dropped, no-new-privileges, a bounded PID limit, and no Docker socket or host filesystem mounts. No build argument, label, layer, or image environment contains an application secret.

Environment contract

Values are read at process start; changing any value requires a container restart. Empty optional values mean the shown default/disabled behavior.

Production database and secrets

Variable Production contract Default Secret Effect
PORTFOLIOHUB_DB_ENABLED required; true for persistent production false no Selects PostgreSQL instead of in-memory repository
TIMESCALEDB_HOST required when DB enabled localhost no PostgreSQL host
TIMESCALEDB_PORT optional 5432 no PostgreSQL port
TIMESCALEDB_NAME required when DB enabled portfoliohub no database name
TIMESCALEDB_USER required when DB enabled portfoliohub usually sensitive database user
TIMESCALEDB_PASSWORD required by production DB policy empty yes database password
TIMESCALEDB_SSLMODE optional; set explicitly in production prefer no psycopg SSL mode
TIMESCALEDB_CONNECT_TIMEOUT_SEC optional 10 no connection timeout
TIMESCALEDB_APPLICATION_NAME optional portfoliohub no PostgreSQL application name
CREDENTIAL_ENCRYPTION_KEY required when DB enabled empty yes Fernet key for stored broker credentials; startup fails closed if absent

Authentication, bootstrap, and HTTP behavior

Variable Required/default Secret Effect
PORTFOLIOHUB_ALLOWED_ORIGINS optional; http://localhost:3000 no comma-separated CORS origins; set the trex-web origin in production
PORTFOLIOHUB_ROOT_PATH optional; empty no reverse-proxy ASGI root path; does not rewrite the actual route paths
PORTFOLIOHUB_READ_TOKENS optional; empty yes comma-separated legacy read service tokens
PORTFOLIOHUB_TRADE_WRITE_TOKENS optional; empty yes comma-separated legacy ledger-write service tokens
PORTFOLIOHUB_ADMIN_TOKENS optional; empty yes comma-separated legacy admin service tokens
PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS optional; 12 no login session lifetime
PORTFOLIOHUB_CLIENT_TOKEN_DEFAULT_TTL_DAYS optional; 365 no retained client-token default
PORTFOLIOHUB_BOT_TOKEN_DEFAULT_TTL_DAYS optional; 365 no bot-token default
PORTFOLIOHUB_BOOTSTRAP_USERNAME optional; empty no with password, creates the first admin only when no users exist
PORTFOLIOHUB_BOOTSTRAP_EMAIL optional; empty personal data bootstrap user email
PORTFOLIOHUB_BOOTSTRAP_PASSWORD optional; empty yes bootstrap password; inject temporarily and remove after first successful start
PORTFOLIOHUB_ENVIRONMENT optional; development no reported environment metadata
PORTFOLIOHUB_APP_NAME optional; PortfolioHub no application/OpenAPI display name
PORTFOLIOHUB_APP_VERSION do not override for v0.1.2 0.1.2 no

REDIS_HOST, REDIS_PORT, REDIS_DB, REDIS_PASSWORD, LOKI_HOST, LOKI_PORT, and LOKI_ENABLED are still parsed for source compatibility but the inspected v0.1.2 application does not open Redis or Loki connections. They are not production dependencies of this image.

Legacy and sibling mapping

The existing database variable names remain TIMESCALEDB_*; v0.1.2 deliberately does not invent a DATABASE_URL alias. Existing PORTFOLIOHUB_{READ,TRADE_WRITE,ADMIN}_TOKENS Bearer-token behavior remains compatible. trex-core's TREX_PORTFOLIO_HUB_BASE_URL and TOML portfolio_hub_token, and trex-web's server-only PORTFOLIO_HUB_API_BASE_URL, are caller-side settings: point them to http://portfolio-hub:8000 (or the equivalent service URL) and do not inject them into this container.

Liveness and readiness

  • Liveness: unauthenticated GET /health must return HTTP 200 with status=ok. It is intentionally process-only and does not query PostgreSQL. db_enabled only reports configuration; it is not readiness.
  • Readiness: run pfhub --format json health-check with the same runtime environment. With DB enabled it performs a repository ping, reads alembic_version without migration, compares it with the single packaged Alembic head, and exits nonzero on connectivity, missing schema, multiple heads, or revision mismatch.
  • API identity: GET /openapi.json is stable for smoke verification. Health version is 0.1.2.

Do not route traffic from a static /health success alone. Readiness checks never apply, stamp, or repair migrations.

One-shot migration and 0028 -> 0029 policy

The installed image contains exactly 29 migration files and one Alembic head, 20260814_0029. The application default command does not invoke Alembic. Migration is a separate pre-start operation using the exact same digest and database environment:

  1. back up the database and record the current immutable image digest and alembic_version;
  2. require the normal upgrade origin 20260715_0028; for an older revision, review and apply the full ordered chain first;
  3. stop writes or otherwise establish an operator-approved maintenance boundary;
  4. execute pfhub migrate-db once with restart disabled;
  5. stop rollout on any nonzero result and preserve sanitized logs;
  6. run pfhub --format json health-check and require both database_revision and expected_revision to equal 20260814_0029;
  7. start application replicas and verify /health, /openapi.json, authenticated caller paths, and strategy data;
  8. a repeated pfhub migrate-db is expected to be idempotent and remain at 0029, but it is verification—not an application-start hook.

Migration 0029 is additive/idempotent around the strategy catalog: it preserves existing strategy IDs and portfolio references, reuses the canonical target_value/{} strategy, and backfills catalog data. Before migration, inspect duplicate or invalid strategy references and take a backup. After migration, verify strategy listing and portfolio active-strategy fields before resuming writes.

v0.1.2 introduces no schema migration: the packaged inventory remains exactly 29 migration files and the single head remains 20260814_0029. Operators already at 0029 must not expect or manufacture a new revision for this patch release.

A pre-Alembic legacy schema is detected only when all three tables strategy_sets, portfolios, and portfolio_ledger exist; the CLI stamps 0027 and then runs real upgrades 0028 and 0029. This compatibility path must be used only after backup and schema review. A partially matching schema is not auto-stamped and must fail for operator investigation.

Image rollback is not database rollback. Selecting older application bytes does not reverse schema/data changes, tokens, or side effects. Database restore, downgrade, or forward-fix requires separate review and authorization; no automatic downgrade is provided.

API verification and known caller blockers

v0.1.2 preserves Bearer authentication and the caller route surface observed in sibling source. The release gate runs the complete portfolio-hub source test suite (including auth success/failure, strategy catalog/data, portfolio and ledger APIs), then verifies the built image's health/OpenAPI surface, exact 29-file migration inventory, isolated PostgreSQL migration twice, readiness at 0029, persistent-volume restart, non-root/read-only operation, secret canary absence, and graceful SIGTERM. Publication consumes the tested archive without rebuilding it.

The strategy catalog API change is additive: strategy responses expose nick, and admin create/update requests may set it. Existing clients may omit nick; supplied values are trimmed and blank-only values are rejected. trex-web v0.2.2 requires this backend contract, but that dependency does not resolve or waive the separate caller blockers below.

Route presence is not a positive cross-repository caller-compatibility claim. Direct source comparison found the following unresolved consumers, which the requester explicitly separated as BLOCKED follow-up rather than adding legacy shims to portfolio-hub:

  • trex-web omits required strategy_id when creating a portfolio, so that mutation currently returns 422;
  • trex-web can omit required account_ref when creating or rotating credentials, so those requests currently return 422;
  • trex-web sends daemon activation state through the general PATCH route, which can return 200 without changing activation; it must use the activate/deactivate routes;
  • portfolio-hub returns singular broker from /v1/user_info, while trex-web types brokers[];
  • portfolio-hub represents duplicate ledger success as status="duplicate", while trex-core currently checks duplicate=true, so the core result classification is inconsistent.

Do not claim trex-core/trex-web compatibility or switch production callers until those consumer changes and cross-repository tests are completed.