Skip to content

Deploying changes to test and exercising the app

End-to-end workflow for deploying a branch to the test server (10.1.1.20) and smoke-testing it before promoting to production.

Companion doc: test-server-and-pptx-remediate.md β€” covers server architecture, service URLs, and recovery.

One-time setup per app

An app is deployable once it has: a Dockerfile, a .staging-deploy manifest, and (optionally for Node apps) an npm run stage script. None of the apps in this monorepo have this wired up yet β€” do it once per app.

Example for apps/web:

1. apps/web/.staging-deploy

APP_NAME=web # deploys to https://web.test.lan
APP_PORT=3000 # whatever your container exposes
DOCKERFILE=Dockerfile
BUILD_CONTEXT=.
ENV_FILE=.env.staging # optional β€” shipped to the server, loaded by the container

2. Working Dockerfile at the app root

deploy-to-staging runs docker build in BUILD_CONTEXT with DOCKERFILE. Whatever you build locally is what ships. Keep build context slim with a thorough .dockerignore.

3. Node apps: add npm run stage

In apps/web/package.json:

"scripts": {
"stage": "deploy-to-staging"
}

Python services skip this β€” just run deploy-to-staging directly from the service dir.

4. .env.staging (optional)

Environment values specific to the test box β€” Supabase URL/keys, LocalStack endpoint, third-party API keys. Keep it gitignored. It’s rsync’d to the server and injected into the container at runtime.

5. Pptx-remediate caveat

The deploy CLI defaults to --host test (10.1.1.20), but pptx-remediate runs on .17. If you wire it up to this pipeline, set STAGING_SSH=staging in the manifest (or pass --host staging) so it targets the right box.

Per-change workflow

1. Branch and commit

Post-launch work lives on branches, not main (per global standards).

Terminal window
git checkout -b feature/some-change
# edit, edit
git add -A && git commit -m "Add X"
git push -u origin HEAD

2. Run tests locally first

Fast feedback before spending a deploy cycle.

Terminal window
cd apps/web
npm test
npm run typecheck
npm run lint

3. Deploy to test

From the app’s directory:

Terminal window
cd apps/web
npm run stage # Node apps
# or
deploy-to-staging # Python services (or any Dockerfile-only project)

What happens under the hood:

  1. Reads .staging-deploy
  2. Runs docker build on your Mac (build errors surface immediately)
  3. Saves the image to a tarball and streams it to test over SSH (no registry needed)
  4. docker loads the image on the server
  5. Writes /srv/staging/apps/web/docker-compose.yml with your image + .env.staging
  6. Writes /srv/staging/caddy/apps.d/web.caddy to route web.test.lan β†’ your container
  7. docker compose up -d β€” starts/rolls the container
  8. Reloads Caddy via its admin API (zero-downtime)

End state: branch code live at https://web.test.lan with valid mkcert TLS.

4. Exercise the app

Hit the standards your CLAUDE.md requires:

Terminal window
# Manual walkthrough
open https://web.test.lan
# Automated checks (run locally, pointed at the staged URL)
PLAYWRIGHT_BASE_URL=https://web.test.lan npm run test:a11y
PLAYWRIGHT_BASE_URL=https://web.test.lan npm run test:mobile
# Health endpoint
curl -sI https://web.test.lan/health
# Logs (centralized in Loki on .17, dashboarded on .20's Caddy)
open https://grafana.test.lan # Explore β†’ Loki β†’ {container="web"}
ssh test 'docker logs -f web' # or tail directly
# Uptime monitor
open https://kuma.test.lan

If the app uses staged Supabase, LocalStack, or MinIO:

Terminal window
# Supabase Studio β€” poke the staged DB
open https://studio.test.lan
# LocalStack β€” list S3 buckets, Lambdas, etc.
awslocal s3 ls --endpoint-url https://localstack.test.lan
awslocal lambda list-functions --endpoint-url https://localstack.test.lan
# MinIO object browser
open https://minio-console.test.lan

6. Iterate

deploy-to-staging is idempotent. Edit β†’ commit (or don’t) β†’ re-run it. Each cycle is a full rebuild + container rollover, ~30 s to 2 min depending on Dockerfile layer caching.

7. Promote

If it works on test, open a PR into main, get it reviewed, merge, then the app’s production deploy process takes over (varies per app β€” Cloudflare Pages for apps/home, AWS Lambda for the API, etc.).

Common troubleshooting

SymptomLikely cause / fix
deploy-to-staging: command not foundSymlink /usr/local/bin/deploy-to-staging is broken. Re-link to ~/Projects/staging-server-setup/scripts/deploy-to-staging.sh.
Build works on Mac but fails on server (or vice versa)Architecture mismatch on Apple Silicon. Set DOCKER_DEFAULT_PLATFORM=linux/amd64 or add --platform linux/amd64 to the Dockerfile FROM.
App starts but web.test.lan returns 502APP_PORT in .staging-deploy doesn’t match what the container actually listens on. Check docker logs <name> on the server.
Supabase anon/service keys changedWhen Supabase is reset, keys rotate. Fresh values live in /srv/staging/supabase/.env on test. Update .env.staging and redeploy.
New image not picked up / stale containerForce rollover: ssh test 'cd /srv/staging/apps/web && docker compose down && docker compose up -d'.
Caddy won’t route a new hostnameEntry missing from /etc/hosts on your Mac. Add 10.1.1.20 <name>.test.lan to /etc/hosts.
Logs don’t appear in GrafanaPromtail on test might not be running, or its config isn’t pushing to http://10.1.1.17:3100. ssh test 'docker logs staging-promtail --since 1m'.

Resetting the test environment

Because the test box is meant to be ephemeral, reset anything that gets in a weird state:

Terminal window
# LocalStack to pristine
ssh test 'cd /srv/staging/localstack && docker compose down && sudo rm -rf data/* && docker compose up -d'
# Supabase (wipes DB)
ssh test 'cd /srv/staging/supabase && docker compose down -v && docker compose up -d'
# A deployed app
ssh test 'cd /srv/staging/apps/<name> && docker compose restart'
# Full nuke and redeploy of an app
ssh test 'cd /srv/staging/apps/<name> && docker compose down && rm -rf /srv/staging/apps/<name>'
# then locally: cd apps/<name> && npm run stage