Skip to content

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

bash
# 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=50

Build scripts

ScriptImage
./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.

bash
doctl registry login          # once per session

Each 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:

bash
TAG=$(git rev-parse --short HEAD)
./scripts/build-api.sh "$TAG"
kubectl set image deployment/playpalz-api api=registry.digitalocean.com/playpalzproduction/api:$TAG

kubectl 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:

  1. Apply the migration (see below) — additive changes first.
  2. Deploy the services that depend on it.
  3. 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:

bash
# 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.prisma

More detail in Migrations & Seeding.

Secrets

Adding or changing a secret does not restart anything:

bash
kubectl apply -f infra/k8s/sealed-secrets/<name>.yaml
kubectl rollout restart deployment/<every-consumer>

See Secrets Management.

Infrastructure changes

bash
cd infra/terraform
make plan            # read it carefully
make apply

Watch 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

bash
kubectl rollout undo deployment/playpalz-api
kubectl rollout status deployment/playpalz-api

With :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-types passes
  • [ ] pnpm lint passes
  • [ ] apps/ogun and apps/anansi test 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 login done
  • [ ] Image built and pushed
  • [ ] kubectl rollout status reports 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.

Internal documentation — PlayPalz platform