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 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.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 /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.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:
- back up the database and record the current immutable image digest and
alembic_version; - require the normal upgrade origin
20260715_0028; 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 equal20260814_0029; - 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 at0029, 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_idwhen creating a portfolio, so that mutation currently returns422; - 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.