Deploying
There is no CI/CD. Deploys are run from a developer machine: build the image, push it, restart the deployment.
What this means in practice
Nothing verifies that tests pass, types check, or the right branch is checked out before code reaches production. The build happens against whatever is in your working tree. Run the checks yourself before you build — the checklist below is the only gate that exists.
Standard deploy
# 1. Confirm what you are shipping
git status # clean tree?
git log --oneline -5
# 2. Verify
pnpm check-types
pnpm lint
cd apps/ogun && pnpm test && cd ../anansi && pnpm test && cd ../..
# 3. Build and push
./scripts/build-api.sh
# 4. Roll out
kubectl rollout restart deployment/playpalz-api
kubectl rollout status deployment/playpalz-api
# 5. Verify
curl https://api.playpalz.gg/health
kubectl logs -l app=playpalz-api --tail=50Build scripts
| Script | Image |
|---|---|
./scripts/build-api.sh [tag] | …/api |
./scripts/build-web.sh [tag] | …/web |
./scripts/build-esu.sh [tag] | …/esu |
./scripts/build-ogun.sh [tag] | …/ogun |
./scripts/build-anansi.sh [tag] | …/anansi |
./scripts/build-igdb-heartbeat.sh [tag] | …/igdb-heartbeat |
./scripts/build-all.sh [tag] | All six |
Registry: registry.digitalocean.com/playpalzproduction. Tag defaults to latest.
doctl registry login # once per sessionEach script builds with --platform linux/amd64 from the repo root as context. Do not run docker build by hand and forget that flag — an arm64 image from an Apple Silicon machine fails on the cluster with exec format error.
Why a restart rather than an apply
Manifests pin :latest with imagePullPolicy: Always. Since the tag does not change, kubectl apply sees no diff and does nothing. kubectl rollout restart creates new pods, which re-pull latest.
:latest costs you rollback
You cannot tell which build a running pod has, and kubectl rollout undo only returns to the previous pod template — which points at the same tag. A real rollback means rebuilding from the old commit.
Moving to versioned tags fixes this:
TAG=$(git rev-parse --short HEAD)
./scripts/build-api.sh "$TAG"
kubectl set image deployment/playpalz-api api=registry.digitalocean.com/playpalzproduction/api:$TAGkubectl rollout undo then genuinely reverts, because the previous template names a different image.
Deploy order
Dependencies matter when a change spans services.
Database migration involved:
- Apply the migration (see below) — additive changes first.
- Deploy the services that depend on it.
- A destructive migration comes after the code that stopped using the old shape is fully rolled out.
Shared package change (@playpals/types, @playpals/db, @playpals/queue): rebuild and deploy every service that imports it. The package is compiled into each image, so a partial deploy leaves services disagreeing about a contract.
Realtime contract change: deploy esu and api together. A mobile client change may need to wait for app store review, so make socket changes backward-compatible.
Applying migrations
The API image does not migrate at startup. Do it deliberately:
# 1. snapshot first
doctl databases backups list <database-id>
# 2. apply
kubectl run prisma-migrate --rm -it \
--image=registry.digitalocean.com/playpalzproduction/api:latest \
--env="DATABASE_URL=$DATABASE_URL" \
--restart=Never \
-- npx prisma migrate deploy --schema packages/database/prisma/schema.prismaMore detail in Migrations & Seeding.
Secrets
Adding or changing a secret does not restart anything:
kubectl apply -f infra/k8s/sealed-secrets/<name>.yaml
kubectl rollout restart deployment/<every-consumer>See Secrets Management.
Infrastructure changes
cd infra/terraform
make plan # read it carefully
make applyWatch for destroy and then create replacement in the plan. See Terraform.
Mobile releases
Separate pipeline entirely — EAS Build and EAS Submit. See Builds & Releases.
Note the coupling: an app release can take days to clear review, so backend changes must remain compatible with the currently released app version. Never ship a breaking API change and the app that needs it at the same time.
Rollback
kubectl rollout undo deployment/playpalz-api
kubectl rollout status deployment/playpalz-apiWith :latest this only helps if the previous pods had an older image cached. The reliable path is to check out the last good commit, rebuild, push, and restart.
Migrations do not roll back. Prisma has no down-migrations here. Reverting a schema change means writing a new forward migration, or restoring from a snapshot and losing everything since.
Deploy checklist
- [ ] Working tree clean, on the intended commit
- [ ]
pnpm check-typespasses - [ ]
pnpm lintpasses - [ ]
apps/ogunandapps/anansitest suites pass (the API has no tests — see Testing) - [ ] Migrations committed and applied in the right order
- [ ] New environment variables added to the deployment manifest and any sealed secret
- [ ]
doctl registry logindone - [ ] Image built and pushed
- [ ]
kubectl rollout statusreports success - [ ] Health endpoint returns OK
- [ ] Logs checked for new errors
- [ ] Docs updated in
apps/docs/
The obvious improvement
A GitHub Actions workflow that runs type-checks, lint, and tests on every PR, and on merge to master builds images tagged with the commit SHA, pushes them, and updates the deployment. That removes the laptop from the critical path, gives every deploy a traceable artifact, and makes rollback a one-line command.
Nothing about the current setup makes this hard — the build scripts already do the work.
