14 KiB
btcdata Kubernetes deployment plan
Goal and interpretation
Deploy btcdata through the existing Flux-managed two-node k3s cluster (srv1893094 and citadel), with exactly one btcdata pod running at a time. The pod may run on either node and Kubernetes may recreate it on the other node after failure.
The current Compose instance is assumed to be /home/pmp/apps/btcdata on citadel. It must be stopped and verified stopped before the Kubernetes replica is increased from zero to one, because btcdata runs Binance, Kraken, Strike, and currency-rate writers.
Production traffic remains internal: btcprice proxies all browser HTTP/WebSocket traffic to btcdata. btcdata does not need an Ingress.
A one-replica Deployment is active/passive scheduling, not a warm standby. It normally prevents duplicate writers, but it cannot strictly fence a partitioned old process. If strict single-writer guarantees are required, add PostgreSQL advisory-lock leader election before deployment.
Target architecture
btcprice pods (namespace btcprice)
-> http://btcdata.btcdata.svc.cluster.local:4101
-> ClusterIP Service btcdata
-> one btcdata pod on srv1893094 OR citadel
-> PostgreSQL btcprod on citadel
-> Frankfurter Service in namespace btcdata
-> Binance, Kraken, and Strike APIs
Kubernetes objects and image automation belong in magnus/deploy; application image building belongs in this repository. Do not deploy the workload with Compose or direct kubectl apply.
Important limitations
- PostgreSQL remains on citadel and is still a single point of failure. Moving the btcdata pod to
srv1893094does not help if citadel or its database is unavailable. - The database must be reachable from both k3s nodes. Use a stable reachable address, preferably citadel's NetBird address, rather than the LAN-only
192.168.50.195, after verifying PostgreSQL listens on and permits that path. - One replica causes a short interruption during upgrades and node failover.
btcpriceshould become unready and reconnect when btcdata returns. - Kubernetes
Deploymentwith one replica is not absolute split-brain protection during a network partition. PostgreSQL advisory locking is the recommended follow-up.
Phase 1 — preflight and decisions
-
Synchronize the GitOps checkout before editing:
git -C ../deploy switch main git -C ../deploy pull --ff-only -
Confirm both nodes are Ready and amd64:
srv1893094citadel
-
From a disposable pod pinned to each node, verify:
- PostgreSQL TCP connectivity;
- authentication to
btcprodwith a temporary, non-logged command; - outbound HTTPS/WebSocket access to Binance, Kraken, Strike, and Harbor.
-
Decide and record the production JDBC hostname. Do not put the password in Git.
-
Confirm the legacy workload is the Compose project at
/home/pmp/apps/btcdataon citadel and record:- current image ID;
- effective non-secret environment;
- container names and Compose project name;
- a tested restart command for rollback.
-
Back up
btcprodand test that the backup can be read/restored. -
Confirm whether strict fencing is required. If yes, implement a PostgreSQL advisory lock so only the lock holder starts all writer components before proceeding.
Gate: both nodes can reach PostgreSQL and required external APIs, a database backup exists, and the exact legacy stop/rollback commands are known.
Phase 2 — application readiness
- Run the existing test suite with
make test. - Add or confirm tests for startup, shutdown, migrations, health, and disabled pollers.
- Add a dependency-aware
/api/readyendpoint if practical. It should check the database and required in-process state without performing external API calls. Keep/api/healthprocess-only for liveness. - Ensure SIGTERM shuts down Undertow and all polling futures within the Kubernetes termination grace period.
- Review database migrations for backward compatibility so rollback to the Compose image remains possible.
- Keep runtime configuration in environment variables:
BTCDATA_PORT=4101JDBC_URLfrom a SecretSTRIKE_API_KEYfrom a SecretCORS_ORIGINrestricted to the actual frontend origins if direct browser access is ever enabledFRANKFURTER_URL=http://frankfurter:8080BINANCE_ENABLED=trueKRAKEN_ENABLED=trueSTRIKE_ENABLED=true
Gate: tests pass, termination is clean, and the production image can start with no embedded credentials.
Phase 3 — CI image publication
Create .gitea/workflows/build.yml, following the established btcprice pipeline:
- Trigger on
mainandv*tags. - Run
make test. - Build
linux/amd64from the existing multi-stageDockerfile. - Publish to
harbor.pmagnus.cloud/btcdata/btcdatausing the Harbor CI robot. - Publish immutable tags:
main-YYYYMMDDhhmmssmain-YYYYMMDDhhmmss-<short-sha>
- Wait for Harbor scanning/policy approval before creating the companion tag.
- Configure Harbor scan-on-push, vulnerability blocking, retention, and garbage collection consistently with btcprice.
- Verify both k3s nodes can pull the approved digest with the existing pull credentials.
Do not give CI a kubeconfig or a GitOps repository write key. Flux remains responsible for deployment.
Gate: an approved immutable image is available and pullable from both nodes.
Phase 4 — secrets
The deploy repository currently has no encrypted-secret mechanism. Until SOPS or an external secret operator is introduced:
- Create a
btcdatanamespace. - Provision a
btcdata-runtimeSecret out of band, using stdin or a temporary mode-0600file, with:JDBC_URLSTRIKE_API_KEY
- Never put decoded values, command output, or generated Secret YAML in either repository.
- Document how to recreate the Secret after cluster recovery in a protected operations document.
- Reference the Secret with
env[].valueFrom.secretKeyRefin the Deployment.
Prefer adding SOPS/age to Flux as a follow-up so secret recovery is declarative and encrypted.
Phase 5 — GitOps manifests, initially scaled to zero
Create ../deploy/apps/btcdata/:
apps/btcdata/
namespace.yaml
deployment.yaml
service.yaml
frankfurter-deployment.yaml
frankfurter-service.yaml
pdb.yaml
kustomization.yaml
Update:
../deploy/apps/kustomization.yamlto includebtcdata;../deploy/infrastructure/image-automation/kustomization.yaml;- add
../deploy/infrastructure/image-automation/btcdata-image.yaml.
btcdata Deployment
Use:
- namespace
btcdata; - image
harbor.pmagnus.cloud/btcdata/btcdata:<approved-main-tag>; - Flux image-policy marker for
flux-system:btcdata; replicas: 0for the initial reconciliation;strategy.type: Recreateto avoid overlapping writer pods during normal upgrades;- node affinity allowing only
srv1893094andcitadel; - container port
4101, namedhttp; terminationGracePeriodSeconds: 30or longer if shutdown testing requires it;- startup and liveness probes on
/api/health; - readiness on
/api/readyif added, otherwise/api/healthtemporarily; - conservative initial requests/limits, then tune from observed usage;
- no host ports, host networking, persistent volume, or Ingress.
A one-replica PDB with minAvailable: 1 protects against accidental voluntary eviction, but it also blocks a normal drain. Document the controlled PDB override/removal required for planned node maintenance.
Service
Create a selector-based ClusterIP Service named btcdata in namespace btcdata, port 4101 to the named http container port.
Frankfurter
Deploy lineofflight/frankfurter behind a ClusterIP Service named frankfurter on port 8080. Pin an immutable image version or digest rather than a floating tag. One replica is sufficient because btcdata retains rates in PostgreSQL and can retry temporary failures.
Image automation
Add an ImageRepository and ImagePolicy matching only plain main tags:
filterTags:
pattern: '^main-(?P<ts>[0-9]{14})$'
extract: '$ts'
policy:
numerical:
order: asc
Validate all kustomizations locally. Commit and push to magnus/deploy; do not manually apply the workload. Confirm Flux creates the namespace, Service, Frankfurter, and a zero-replica btcdata Deployment.
Gate: Flux is Ready, Frankfurter is healthy, the btcdata Service exists, and no btcdata Kubernetes pod is running.
Phase 6 — controlled cutover
This sequence deliberately includes a short outage to guarantee that the old and new writers do not intentionally overlap.
- Pause Flux btcdata image automation for the cutover window, or verify that no new image can be selected during the procedure.
- On citadel, stop only the legacy btcdata Compose project in
/home/pmp/apps/btcdata.- Do not stop PostgreSQL.
- Do not remove
btc-network, images, volumes, or deployment files.
- Verify before Kubernetes startup:
- the btcdata Compose container is stopped;
- port
4102is no longer listening for btcdata; - no legacy Java btcdata process remains;
- database write timestamps stop advancing except for expected external writers.
- In
magnus/deploy, change the btcdata Deployment fromreplicas: 0toreplicas: 1, commit, and push. - Let Flux reconcile. Do not use
kubectl scale, because Git would revert it. - Confirm exactly one pod is Running and Ready on either allowed node.
- Check startup logs for successful migrations, database connection, Binance WebSocket, Kraken pollers, Frankfurter rates, and Strike ticker.
- From disposable pods pinned to each node, test the new Service FQDN:
/api/health;/api/price/latest;/api/kraken-minuteand/api/kraken-hour;/api/price/wsand/api/strike/wsWebSockets.
- Confirm database rows advance once at the expected cadence and no duplicate-writer symptoms appear.
Gate: one Kubernetes pod is healthy, data writes are correct, and both nodes can reach its ClusterIP Service.
Phase 7 — switch btcprice to Kubernetes btcdata
Modify ../deploy/apps/btcprice/ in one GitOps change:
-
Change the Deployment environment to:
BTCDATA_URL=http://btcdata.btcdata.svc.cluster.local:4101 -
Remove the legacy selectorless
btcdataService from thebtcpricenamespace. -
Remove
btcdata-endpointslice.yamland its kustomization entry. -
Keep btcprice's existing two-replica scheduling, readiness behavior, and WebSocket ingress timeouts.
-
Validate both application kustomizations and push the change to
magnus/deploy. -
Let Flux roll out btcprice and verify:
- both btcprice pods become Ready;
/api/readysucceeds;- chart/API requests return current data;
/ws/priceand/ws/strikereconnect and continue updating;https://btcprice.k8s.pmagnus.cloudandhttps://btc.pmagnus.cloudwork.
After successful validation, re-enable btcdata image automation.
Phase 8 — failover and acceptance tests
- Delete the btcdata pod and verify Kubernetes creates one replacement, never a steady state of two Ready replicas.
- Cordon the node currently hosting btcdata, perform a controlled pod deletion, and verify it starts on the other node. Uncordon afterward.
- Confirm btcprice becomes temporarily unready during failover and recovers without manual changes.
- Verify WebSocket clients reconnect and fresh price data resumes.
- Perform an automated image update and confirm
Recreatestops the old pod before starting the new pod. - Review logs and database data for duplicate Binance/Kraken/Strike writes.
- Observe CPU, memory, restart count, startup time, and failover duration; tune resources and probes.
- Keep the stopped Compose files and image for a defined rollback window, at least 24 hours.
Acceptance criteria:
- Flux reports all resources Ready.
- Exactly one btcdata pod runs across the two eligible nodes.
- btcprice uses only Kubernetes DNS for btcdata.
- HTTP and WebSocket functions work through both production hostnames.
- A source push can reach production through Gitea, Harbor, and Flux without SSH deployment or manual
kubectl apply. - The legacy Compose btcdata remains stopped.
Rollback
Rollback must also preserve the single-writer rule:
- Change the Kubernetes btcdata Deployment to
replicas: 0inmagnus/deploy, commit, and wait for Flux. - Verify no btcdata pod or process remains in Kubernetes.
- Restore btcprice's selectorless Service and EndpointSlice pointing to citadel, and restore
BTCDATA_URL=http://btcdata:4102through GitOps. - Restart the recorded btcdata Compose service on citadel.
- Verify health, HTTP/WebSocket data, and database write cadence.
- Keep the failed Kubernetes image and logs for diagnosis; do not rewrite Git history or delete Harbor evidence.
For an application-only rollback, pause image automation, commit the previous approved immutable image tag to magnus/deploy, and let Flux perform the Recreate rollout.
Follow-up improvements
- Add PostgreSQL advisory-lock leader election, then consider two btcdata pods with one writer leader and one standby/API replica.
- Move PostgreSQL to a highly available service reachable independently of citadel.
- Add SOPS/age or an external secret operator.
- Add dependency-aware readiness and metrics/alerts for stale prices, failed pollers, database errors, and leader status.
- Update this repository's
README.mdandCLAUDE.md, btcprice documentation, and system infrastructure documentation after the rollback window closes. - Retire
deploy-citadel.shand legacy Compose deployment files only after rollback is no longer required.