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.lanAPP_PORT=3000 # whatever your container exposesDOCKERFILE=DockerfileBUILD_CONTEXT=.ENV_FILE=.env.staging # optional β shipped to the server, loaded by the container2. 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).
git checkout -b feature/some-change# edit, editgit add -A && git commit -m "Add X"git push -u origin HEAD2. Run tests locally first
Fast feedback before spending a deploy cycle.
cd apps/webnpm testnpm run typechecknpm run lint3. Deploy to test
From the appβs directory:
cd apps/webnpm run stage # Node apps# ordeploy-to-staging # Python services (or any Dockerfile-only project)What happens under the hood:
- Reads
.staging-deploy - Runs
docker buildon your Mac (build errors surface immediately) - Saves the image to a tarball and streams it to
testover SSH (no registry needed) docker loads the image on the server- Writes
/srv/staging/apps/web/docker-compose.ymlwith your image +.env.staging - Writes
/srv/staging/caddy/apps.d/web.caddyto routeweb.test.lanβ your container docker compose up -dβ starts/rolls the container- 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:
# Manual walkthroughopen https://web.test.lan
# Automated checks (run locally, pointed at the staged URL)PLAYWRIGHT_BASE_URL=https://web.test.lan npm run test:a11yPLAYWRIGHT_BASE_URL=https://web.test.lan npm run test:mobile
# Health endpointcurl -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 monitoropen https://kuma.test.lan5. Inspect related systems
If the app uses staged Supabase, LocalStack, or MinIO:
# Supabase Studio β poke the staged DBopen https://studio.test.lan
# LocalStack β list S3 buckets, Lambdas, etc.awslocal s3 ls --endpoint-url https://localstack.test.lanawslocal lambda list-functions --endpoint-url https://localstack.test.lan
# MinIO object browseropen https://minio-console.test.lan6. 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
| Symptom | Likely cause / fix |
|---|---|
deploy-to-staging: command not found | Symlink /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 502 | APP_PORT in .staging-deploy doesnβt match what the container actually listens on. Check docker logs <name> on the server. |
| Supabase anon/service keys changed | When 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 container | Force rollover: ssh test 'cd /srv/staging/apps/web && docker compose down && docker compose up -d'. |
| Caddy wonβt route a new hostname | Entry missing from /etc/hosts on your Mac. Add 10.1.1.20 <name>.test.lan to /etc/hosts. |
| Logs donβt appear in Grafana | Promtail 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:
# LocalStack to pristinessh 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 appssh test 'cd /srv/staging/apps/<name> && docker compose restart'
# Full nuke and redeploy of an appssh test 'cd /srv/staging/apps/<name> && docker compose down && rm -rf /srv/staging/apps/<name>'# then locally: cd apps/<name> && npm run stage