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:

StagePurpose
lintBasic backend code errors
testAPI logic tests including Postgres and Redis
localization validateApp string catalog and documented key validation
API shape preservationAPI contract and generated artifact preservation
manifest validationKustomize/Kubernetes manifest build check
image buildbackend image creation
security artifactsSupply-chain checks such as SBOM, image scan, and signing
deploy repo updateUpdate 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

Share

Image preview