Leaving Deployment as Records, Not Memory
Tests, images, desired state, and migration order became one repeatable release path
When developing alone, deployment can easily feel lightweight. Tests pass locally, the server receives the code, the screen opens, and it feels done.
Hangangjari could not stay that way. It is an API called by an App Store app, and behind the API are workers, DB migrations, Redis cache, forecasts, and push. If deployment shakes, not one screen but the whole act of collecting and showing data can shake.
So the Hangangjari backend deploys through CI, a container registry, a deploy repository, ArgoCD, and K3s. The core is to leave a record of what checks a change passed before reaching the server users use.
Deployment repeats through the same path
Even when working alone, a user-facing API should not depend on “what command did I run yesterday?” The more familiar deployment becomes, the easier it is to repeat small mistakes. So I tied the same checks, image build, and desired-state update into one path.
sequenceDiagram autonumber participant Dev as Developer participant Git as Git server participant CI as CI participant Registry as Container registry participant Deploy as Deploy repository participant Sync as ArgoCD participant Runtime as K3s participant Smoke as Smoke check Dev->>Git: Push change branch Git->>CI: Trigger verification CI->>CI: Basic checks, tests, API contract checks CI->>CI: Generate and validate deploy manifests CI->>Registry: Build and publish versioned image CI->>Deploy: Update production desired state Sync->>Deploy: Read production desired state Sync->>Runtime: Apply order and hooks Sync->>Runtime: Deploy server and worker processes Smoke->>Runtime: Check health and core behavior
Feature branches only verify. Production desired-state updates happen only from the protected default branch. This distinction prevents experimental branches from leaking into deployment.
It also reduces mental load. If tests fail on an experimental branch, I fix them. But if that failure updates a production image or deploy repository, recovery becomes necessary. Small projects need this firewall even more.
What CI blocks before deployment
CI does more than “build success.” Currently it checks roughly:
| Stage | Purpose |
|---|---|
| lint | Basic backend code errors |
| test | API logic tests including Postgres and Redis |
| localization validate | App string catalog and documented key validation |
| API shape preservation | API contract and generated artifact preservation |
| manifest validation | Kustomize/Kubernetes manifest build check |
| image build | backend image creation |
| security artifacts | Supply-chain checks such as SBOM, image scan, and signing |
| deploy repo update | Update image tag in production desired state |
Even for a personal project, this much checking was necessary. Hangangjari is not only an API service. Workers, DB migrations, cache, push, and forecasts move together. A failed deployment can break not only one screen, but also the parts that create data.
The point of adding checks was not to create fear. If the questions before deployment are automated, the human only needs to decide one thing at the end: “Can this change go in front of users now?”
The value the server follows lives in a separate repository
The application code repository and production desired state are separated. CI builds an image and updates the image tag in the deploy repository. ArgoCD reads the deploy repository and reconciles K3s runtime state.
flowchart LR AppRepo["Application repository"] --> CI["CI pipeline"] CI --> Image["Versioned image"] CI --> DeployRepo["Deploy repository<br/>desired state"] DeployRepo --> Sync["ArgoCD"] Sync --> Runtime["K3s cluster"]
This split has clear benefits.
- It is easier to trace which code shipped as which image.
- Production manifest changes and app code changes can be separated.
- Rollback means returning to a previous desired state.
- ArgoCD shows drift between actual cluster state and desired state.
When deployment leaves records, retrospectives also become easier. To answer “when did it start acting strange?”, I can trace code commit, image, desired state, and K3s rollout in one line.
Migration and worker order are checked
Backend deployment is not only changing the API deployment. DB schema, bootstrap jobs, API, and workers have to line up in order.
ArgoCD sync phases and waves express this order. Hangangjari uses that behavior to separate migration/bootstrap from rollout.
flowchart TB Wave1["Pre-sync<br/>migration or bootstrap"] --> Wave2["Core services<br/>Postgres Redis API"] Wave2 --> Wave3["Workers<br/>parking outing forecast push"] Wave3 --> Wave4["Smoke check<br/>health · feature freshness"]
Automation and human confirmation have different jobs. CI repeatedly handles image build and desired-state update. Sync and smoke confirmation are checked explicitly because of their incident impact.
Fully automatic deployment was not always better. For deployments where DB migrations, worker rollouts, and source-data collection move together, it was safer for a person to make the final context check.
Automation reduces repeated work. Manual confirmation remains the step where I review how far the change can affect the system.
iOS and server releases have different timing
iOS app releases do not share the backend’s rhythm. The server can change in minutes, while the app passes through App Store review and user update cycles.
So API contracts are handled conservatively.
- Do not break DTOs already deployed.
- Distinguish optional field additions from required field changes.
- Account for a period where app versions are mixed.
- Server changes can turn on before app rollout.
- Widgets can see snapshots older than the app screen.
This is why metadata such as generated_at, observed_at, freshness, and status is explicit in DTOs. Here, freshness tells how current a value is. The client should not interpret unknown values as success.
Deployment became records, not memory
Even in a personal project, the path to production needed documentation. CI checks not only tests, but also API shape, manifests, images, and supply chain artifacts. The deploy repository separates code deployment from K3s desired state.
Migrations and worker rollout require as much care as API deployment. The server changes quickly, but iOS goes through App Store review and user update timing, so APIs have to survive a compatibility window longer than the server deployment itself.
For Hangangjari, deployment stopped being a file upload. It became an explainable record of change. When something goes wrong, I can check “what changed?” from records instead of memory.
Share
No comments yet. You can leave the first one.
Pending review