Containerizes the momentum monolith and packages every run mode for
deployment in a single Kubernetes namespace, with PostgreSQL hosted in
the cluster.
GitHub Actions workflow publishing to ghcr.io/fez5587/momentum-system
Helm chart (deploy/helm/momentum):
PostgreSQL StatefulSet + PVC (single source of truth; schema
auto-creates from storage/pg_schema.sql on first connect)
App Deployment running run_live_paper.py with the embedded dashboard
(replicas: 1, Recreate; the dashboard's write endpoints need the
in-process TradingExecutionService) + Service/Ingress on 8010, plus a
wait-for-postgres initContainer
CronJobs for journal / eod-replay / nightly-tune (US/Eastern via
spec.timeZone) sharing the app data PVC
ConfigMap (tuning env) + Secret (DATABASE_URL/broker keys, existingSecret
supported); DATABASE_URL built from the in-cluster Postgres service
Optional Grafana provisioned against the in-cluster Postgres
deploy/README.md with install, secrets, and constraints
Containerizes the momentum monolith and packages every run mode for
deployment in a single Kubernetes namespace, with PostgreSQL hosted in
the cluster.
- Dockerfile (python:3.11-slim, non-root, writable /app/data) + .dockerignore
- GitHub Actions workflow publishing to ghcr.io/fez5587/momentum-system
- Helm chart (deploy/helm/momentum):
* PostgreSQL StatefulSet + PVC (single source of truth; schema
auto-creates from storage/pg_schema.sql on first connect)
* App Deployment running run_live_paper.py with the embedded dashboard
(replicas: 1, Recreate; the dashboard's write endpoints need the
in-process TradingExecutionService) + Service/Ingress on 8010, plus a
wait-for-postgres initContainer
* CronJobs for journal / eod-replay / nightly-tune (US/Eastern via
spec.timeZone) sharing the app data PVC
* ConfigMap (tuning env) + Secret (DATABASE_URL/broker keys, existingSecret
supported); DATABASE_URL built from the in-cluster Postgres service
* Optional Grafana provisioned against the in-cluster Postgres
- deploy/README.md with install, secrets, and constraints
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JCVMm2AbmZvvxwLTX6UpNR
copilot-pull-request-reviewer[bot]
(Migrated from github.com)
left a comment
Copy link
Copy source
Pull request overview
This PR adds a container image and deployment packaging to run the Momentum monolith in Kubernetes (single namespace) with an in-cluster PostgreSQL, plus a local docker-compose stack intended to mirror the Helm setup.
Changes:
Added a Python 3.11 slim Docker image for the monolith (non-root, /app/data intended to be writable via volume/PVC).
Added a Helm chart (deploy/helm/momentum) that deploys Postgres (StatefulSet+PVC), the singleton app Deployment, CronJobs, optional Grafana, and supporting ConfigMap/Secret resources.
Added docker-compose and publishing workflow for building/pushing the image to GHCR.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 8 comments.
Show a summary per file
File
Description
Dockerfile
Builds a non-root container image for running the monolith in multiple “run modes”.
.dockerignore
Excludes secrets/runtime data and deployment assets from the image build context.
docker-compose.yml
Local stack: Postgres + app + optional Grafana, plus on-demand job containers.
.github/workflows/docker-publish.yml
Builds and publishes the image to GHCR on push/tag.
deploy/README.md
Kubernetes + Compose usage and operational notes/constraints.
deploy/helm/momentum/Chart.yaml
New Helm chart metadata (incl. kubeVersion constraint).
deploy/helm/momentum/values.yaml
Default values for image, app, Postgres, CronJobs, Grafana, config, secrets.
deploy/helm/momentum/templates/_helpers.tpl
Naming helpers + Secret name selection + Postgres password resolution.
The workflow is configured to build/publish on a specific feature branch name (claude/kubernetes-image-package-kkdkom). Keeping a one-off branch trigger in main will cause unexpected publishes if the branch is reused, and it’s easy to forget to remove later.
The workflow is configured to build/publish on a specific feature branch name (claude/kubernetes-image-package-kkdkom). Keeping a one-off branch trigger in main will cause unexpected publishes if the branch is reused, and it’s easy to forget to remove later.
The pod template annotation claims to roll on config/secret changes, but only the ConfigMap is hashed. As a result, updating the chart-managed Secret (e.g., rotating DB/broker keys) won’t trigger a rollout automatically.
The pod template annotation claims to roll on config/secret changes, but only the ConfigMap is hashed. As a result, updating the chart-managed Secret (e.g., rotating DB/broker keys) won’t trigger a rollout automatically.
The container runs as a non-root user (per the Dockerfile), but the mounted PVC at /app/data will typically be owned by root on first mount. Without a pod securityContext (fsGroup / runAsUser), the app may fail to write logs/state into /app/data at runtime.
The container runs as a non-root user (per the Dockerfile), but the mounted PVC at /app/data will typically be owned by root on first mount. Without a pod securityContext (fsGroup / runAsUser), the app may fail to write logs/state into /app/data at runtime.
This template ranges over a dict of jobs. Go map iteration order is non-deterministic, so the rendered manifest order can change between Helm runs, creating noisy diffs (e.g. in helm template output / CI). Consider iterating a list (or rendering each CronJob explicitly) to keep output stable.
This template ranges over a dict of jobs. Go map iteration order is non-deterministic, so the rendered manifest order can change between Helm runs, creating noisy diffs (e.g. in `helm template` output / CI). Consider iterating a list (or rendering each CronJob explicitly) to keep output stable.
CronJob pods mount the shared /app/data PVC while running as a non-root user (from the image). Without setting fsGroup/runAsUser in the pod securityContext, these jobs may be unable to read/write learned_params.json, logs, tokens, etc. on many storage classes.
CronJob pods mount the shared /app/data PVC while running as a non-root user (from the image). Without setting fsGroup/runAsUser in the pod securityContext, these jobs may be unable to read/write learned_params.json, logs, tokens, etc. on many storage classes.
The Grafana deployment enables anonymous access with Admin role and disables the login form. In a cluster this effectively grants admin access to anyone who can reach the Service (including other pods), which is unsafe by default.
The Grafana deployment enables anonymous access with Admin role and disables the login form. In a cluster this effectively grants admin access to anyone who can reach the Service (including other pods), which is unsafe by default.
DATABASE_URL is constructed via string interpolation without URL-encoding. If a user supplies a Postgres password/username containing reserved URL characters (e.g. ':' or '@'), psycopg2 URL parsing will break and the app won’t be able to connect.
DATABASE_URL is constructed via string interpolation without URL-encoding. If a user supplies a Postgres password/username containing reserved URL characters (e.g. ':' or '@'), psycopg2 URL parsing will break and the app won’t be able to connect.
Grafana is described as optional, but it is enabled by default. Given the current template enables anonymous admin access, default-on Grafana is a risky default for in-cluster installs.
Grafana is described as optional, but it is enabled by default. Given the current template enables anonymous admin access, default-on Grafana is a risky default for in-cluster installs.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Containerizes the momentum monolith and packages every run mode for
deployment in a single Kubernetes namespace, with PostgreSQL hosted in
the cluster.
auto-creates from storage/pg_schema.sql on first connect)
(replicas: 1, Recreate; the dashboard's write endpoints need the
in-process TradingExecutionService) + Service/Ingress on 8010, plus a
wait-for-postgres initContainer
spec.timeZone) sharing the app data PVC
supported); DATABASE_URL built from the in-cluster Postgres service
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01JCVMm2AbmZvvxwLTX6UpNR
Pull request overview
This PR adds a container image and deployment packaging to run the Momentum monolith in Kubernetes (single namespace) with an in-cluster PostgreSQL, plus a local docker-compose stack intended to mirror the Helm setup.
Changes:
/app/dataintended to be writable via volume/PVC).deploy/helm/momentum) that deploys Postgres (StatefulSet+PVC), the singleton app Deployment, CronJobs, optional Grafana, and supporting ConfigMap/Secret resources.Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 8 comments.
Show a summary per file
/app/data.💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
@@ -0,0 +7,4 @@push:branches:- claude/kubernetes-image-package-kkdkom- mainThe workflow is configured to build/publish on a specific feature branch name (claude/kubernetes-image-package-kkdkom). Keeping a one-off branch trigger in main will cause unexpected publishes if the branch is reused, and it’s easy to forget to remove later.
@@ -0,0 +20,4 @@{{- include "momentum.selectorLabels" (dict "ctx" . "component" "app") | nindent 8 }}annotations:# Roll the pod when config/secret content changes.checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}The pod template annotation claims to roll on config/secret changes, but only the ConfigMap is hashed. As a result, updating the chart-managed Secret (e.g., rotating DB/broker keys) won’t trigger a rollout automatically.
@@ -0,0 +25,4 @@{{- with .Values.imagePullSecrets }}imagePullSecrets:{{- toYaml . | nindent 8 }}{{- end }}The container runs as a non-root user (per the Dockerfile), but the mounted PVC at /app/data will typically be owned by root on first mount. Without a pod securityContext (fsGroup / runAsUser), the app may fail to write logs/state into /app/data at runtime.
@@ -0,0 +1,59 @@{{- if .Values.cronjobs.enabled }}{{- $root := . }}{{- $jobs := dict "journal" .Values.cronjobs.journal "eod-replay" .Values.cronjobs.eodReplay "nightly-tune" .Values.cronjobs.nightlyTune }}{{- range $key, $job := $jobs }}This template ranges over a dict of jobs. Go map iteration order is non-deterministic, so the rendered manifest order can change between Helm runs, creating noisy diffs (e.g. in
helm templateoutput / CI). Consider iterating a list (or rendering each CronJob explicitly) to keep output stable.@@ -0,0 +30,4 @@{{- with $root.Values.imagePullSecrets }}imagePullSecrets:{{- toYaml . | nindent 12 }}{{- end }}CronJob pods mount the shared /app/data PVC while running as a non-root user (from the image). Without setting fsGroup/runAsUser in the pod securityContext, these jobs may be unable to read/write learned_params.json, logs, tokens, etc. on many storage classes.
@@ -0,0 +78,4 @@- name: GF_AUTH_ANONYMOUS_ORG_ROLEvalue: "Admin"- name: GF_AUTH_DISABLE_LOGIN_FORMvalue: "true"The Grafana deployment enables anonymous access with Admin role and disables the login form. In a cluster this effectively grants admin access to anyone who can reach the Service (including other pods), which is unsafe by default.
@@ -0,0 +1,26 @@{{- if not .Values.secrets.existingSecret }}{{- $pgPassword := include "momentum.postgresPassword" . }}{{- $pgHost := include "momentum.postgresName" . }}{{- $databaseUrl := printf "postgresql://%s:%s@%s:%v/%s" .Values.postgres.username $pgPassword $pgHost .Values.postgres.port .Values.postgres.database }}DATABASE_URL is constructed via string interpolation without URL-encoding. If a user supplies a Postgres password/username containing reserved URL characters (e.g. ':' or '@'), psycopg2 URL parsing will break and the app won’t be able to connect.
@@ -0,0 +146,4 @@# ----------------------------------------------------------------------------grafana:enabled: trueimage: grafana/grafana:11.2.0Grafana is described as optional, but it is enabled by default. Given the current template enables anonymous admin access, default-on Grafana is a risky default for in-cluster installs.
Ok