12 KiB
PortfolioHub v0.3.0 container contract
Publication is not deployment
Only a requester-authorized push of the strict tag v0.3.0 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.3.0
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.3.0. 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.3.0 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 8000as 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
/tmptmpfs 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
noand 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.3.0 | 0.3.0 |
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.3.0 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.3.0 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 /healthmust return HTTP 200 withstatus=ok. It is intentionally process-only and does not query PostgreSQL.db_enabledonly reports configuration; it is not readiness. - Readiness: run
pfhub --format json health-checkwith the same runtime environment. With DB enabled it performs a repository ping, readsalembic_versionwithout 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.jsonis stable for smoke verification. Health version is0.3.0.
Do not route traffic from a static /health success alone. Readiness checks never apply, stamp, or repair migrations.
One-shot migration and 0029 -> 0031 policy
The installed image contains exactly 31 migration files and one Alembic head, 20260913_0031. The application default command does not invoke Alembic. Migration is a separate pre-start operation using the exact same digest and database environment:
- back up the database and record the current immutable image digest and
alembic_version; - require the normal upgrade origin
20260814_0029; for an older revision, review and apply the full ordered chain first; - stop writes or otherwise establish an operator-approved maintenance boundary;
- execute
pfhub migrate-dbonce with restart disabled; - stop rollout on any nonzero result and preserve sanitized logs;
- run
pfhub --format json health-checkand require bothdatabase_revisionandexpected_revisionto equal20260913_0031; - start application replicas and verify
/health,/openapi.json, authenticated caller paths, and strategy data; - a repeated
pfhub migrate-dbis expected to be idempotent and remain at0031, 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.2.1 includes migration 0030, an idempotent index on (portfolio_id, recorded_at DESC, ledger_entry_id DESC) for unified-ledger keyset reads. Operators at 0029 must run the same-image one-shot migration before rollout; it advances to 20260820_0030 without mutating ledger rows.
Migration 0031 is additive corporate-action storage. Operators must complete the same-image migration through 20260913_0031 before using the corporate-action endpoint; the endpoint is not available for traffic until readiness reports that head.
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 through 0031. 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.3.0 preserves Bearer authentication, retains all-source unified-ledger reads, and adds stock-consolidation corporate-action recording and replay. 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 31-file migration inventory, isolated PostgreSQL migration twice, readiness at 0031, 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 remains additive: strategy responses expose nick, admin create/update requests may set it, and portfolio responses expose the current strategy as strategy_nick. Existing clients may omit nick; supplied values are trimmed and blank-only values are rejected. trex-web v0.2.3 displays strategy_nick, 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 may create a portfolio without
strategy_id; portfolio-hub must preserve that strategy-unassigned request asactive_strategy_id=nullrather than selecting a default strategy implicitly; - trex-web can omit required
account_refwhen creating or rotating credentials, so those requests currently return422; - trex-web sends daemon activation state through the general PATCH route, which can return
200without changing activation; it must use the activate/deactivate routes; - portfolio-hub returns singular
brokerfrom/v1/user_info, while trex-web typesbrokers[]; - portfolio-hub represents duplicate ledger success as
status="duplicate", while trex-core currently checksduplicate=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.