This guide covers local development, isolated E2E, container validation, and self-hosted compatibility. Managed production is Supabase for PostgreSQL/PostGIS, Realtime, and Storage; Railway for API, worker, migrator, and Redis; and Vercel for Admin and Restaurant. See the deployment guide.
Local defaults are intentionally convenient and are not production credentials.
- Docker Desktop/Engine with Compose v2 and Buildx.
- Node.js 22.13+ with Corepack.
- pnpm 11.11.0 from each package's
packageManagerfield. - Flutter SDK for mobile checks.
- FFmpeg only when regenerating documentation GIFs.
Verify:
docker version
docker compose version
docker buildx version
node --version
corepack pnpm --version
flutter --version
ffmpeg -versionCreate ignored local files only when host-run commands need them:
Copy-Item .env.example .env
Copy-Item backend/.env.example backend/.env
Copy-Item web/apps/admin/.env.example web/apps/admin/.env.local
Copy-Item web/apps/restaurant/.env.example web/apps/restaurant/.env.local- Never commit real dotenv files, CLI auth, dumps, certificates, tokens, or signing material.
- Any credential pasted into chat/logs is exposed and must not be reused for production.
- Local defaults (
foodflow_dev,minioadmin, development JWTs) are forbidden in production. - Local provider mode is explicit: Socket.IO + Redis/BullMQ + MinIO.
- Managed production mode is explicit: Supabase Realtime/Storage/Postgres queue, Railway API/worker/migrator/Redis, and Vercel dashboards.
Run PostGIS, Redis, and MinIO in containers while API/web/mobile run on the host:
docker compose up -d postgres redis minioAPI:
cd backend
corepack pnpm install --frozen-lockfile
corepack pnpm prisma generate
corepack pnpm prisma migrate dev
corepack pnpm db:seed
corepack pnpm start:devWeb:
cd web
corepack pnpm install --frozen-lockfile
corepack pnpm devMobile:
cd mobile
flutter pub get --enforce-lockfile
# Android Customer product flavor
flutter run --flavor customer -t lib/main_customer.dart
# Android Driver product flavor
flutter run --flavor driver -t lib/main_driver.dartThese are native device/emulator commands, not local web URLs. The Android flavor names come from mobile/android/app/build.gradle.kts; use the matching explicit Dart entrypoint for iOS development.
Build migration, API, background worker, Admin, and Restaurant from current source:
docker compose up -d --build
docker compose psThe migration container must exit 0 before the API becomes healthy. The worker runs dist/workers/main.js from the backend image, owns durable queue/RAG background work, and has no HTTP port or health endpoint; inspect its startup and RAG logs instead. Web public environment values are build-time values; rebuild a web image after changing NEXT_PUBLIC_*.
Compose builds from the checkout use local revision=local labels. They are useful runtime evidence but not immutable release artifacts; release promotion still needs a frozen sha-<full-commit> build, scan, push, and clean pull.
Default endpoints:
| Service | URL |
|---|---|
| API health | http://localhost:3001/api/healthz |
| Admin health | http://localhost:3000/api/healthz |
| Restaurant health | http://localhost:3002/api/healthz |
| MinIO API/console | http://localhost:9000 / http://localhost:9001 |
Use this overlay when the root stack or another project must remain untouched:
docker compose -f docker-compose.yml -f docker-compose.e2e.yml up -d --build
docker compose -f docker-compose.yml -f docker-compose.e2e.yml psIsolated ports:
| Service | Host port |
|---|---|
| Admin | 13000 |
| API | 13001 |
| Restaurant | 13002 |
| Worker | no host port (internal only) |
| Postgres | 15432 |
| Redis | 16379 |
| MinIO API/console | 19000 / 19001 |
The overlay's CORS contract uses http://localhost:13000 and http://localhost:13002. Use localhost for normal E2E and media capture; 127.0.0.1 intentionally exercises the CORS error state and will not load business data.
Seed the isolated database from the host when required:
$env:DATABASE_URL='postgresql://foodflow:foodflow_dev@localhost:15432/foodflow'
$env:DIRECT_URL=$env:DATABASE_URL
cd backend
corepack pnpm db:seed # compact deterministic fixtures
corepack pnpm db:big-seed # broad dashboard/E2E fixtures when explicitly needed
cd ..
Remove-Item Env:DATABASE_URL,Env:DIRECT_URLBoth seed commands contain deterministic test business data and are blocked from production use. They are test fixtures, not runtime fallback data.
After reseeding an already running overlay, restart the worker so the RAG entry is post-seed; then verify it directly:
docker compose -f docker-compose.yml -f docker-compose.e2e.yml restart worker
docker compose -f docker-compose.yml -f docker-compose.e2e.yml logs --tail 100 workerRequire FoodFlow Worker started and a successful RAG sync complete entry. The worker has no HTTP endpoint, so an HTTP health check would be misleading.
docker compose -f docker-compose.yml -f docker-compose.local.yml up backendThe override mounts backend source/Prisma and runs the builder stage. Do not use it as release evidence.
Release artifacts:
| Target | Dockerfile | Runtime |
|---|---|---|
| Backend | backend/Dockerfile target runner |
distroless Node 22, non-root |
| Migrate | backend/Dockerfile target migrator |
distroless Prisma CLI, non-root |
| Admin | web/apps/admin/Dockerfile |
Next standalone, distroless, non-root |
| Restaurant | web/apps/restaurant/Dockerfile |
Next standalone, distroless, non-root |
There is no generic web/Dockerfile and no separate worker Dockerfile. The worker starts dist/workers/main.js from the backend image.
The web workspace installs Linux/glibc x64 and arm64 native packages so one native builder can produce standalone artifacts for both final architectures. Backend builder toolchains compile missing native modules; final images contain no compiler.
Build both platforms without publishing:
docker buildx build --platform linux/amd64,linux/arm64 \
--target runner --file backend/Dockerfile --cache-to type=cacheonly backend
docker buildx build --platform linux/amd64,linux/arm64 \
--target runner --file web/apps/admin/Dockerfile --cache-to type=cacheonly webThe release workflow additionally runs bcrypt/BullMQ/MessagePack, Prisma CLI, Sharp PNG, non-root UID, manifest, SBOM/provenance, and Trivy checks on both architectures.
Complete gate:
powershell -NoProfile -ExecutionPolicy Bypass \
-File infra/scripts/local-release-gate.ps1 -RunE2EUseful development-only partial run:
powershell -NoProfile -ExecutionPolicy Bypass \
-File infra/scripts/local-release-gate.ps1 \
-AllowDirty -SkipInstall -SkipBuild -SkipDeployPreflightPartial runs must be labeled partial and cannot approve release. The full script covers git/diff hygiene, secret scan, frozen installs, Prisma, backend/web/mobile quality gates, OpenAPI, Compose configs, optional browser E2E, and provider preflights.
With the isolated stack healthy and seeded:
$env:FOODFLOW_ADMIN_URL='http://localhost:13000'
$env:FOODFLOW_RESTAURANT_URL='http://localhost:13002'
$env:FOODFLOW_API_URL='http://localhost:13001/api'
node docs/scripts/capture-product-media.mjs
Remove-Item Env:FOODFLOW_ADMIN_URL,Env:FOODFLOW_RESTAURANT_URL,Env:FOODFLOW_API_URLThe script captures real UI/API states, creates palette-optimized GIFs, removes intermediate frames, and writes a non-secret manifest. Review every resulting screenshot; a successful script can still capture a contract/error state.
docker compose ps
docker compose logs --tail 200 migrate backend worker admin restaurant
Invoke-WebRequest http://localhost:3001/api/healthz
Invoke-WebRequest http://localhost:3000/api/healthz
Invoke-WebRequest http://localhost:3002/api/healthzDo not copy logs into issues/docs until tokens, URLs with credentials, emails, and provider payloads are redacted.
docker compose downstops services and preserves named volumes.docker compose down -vdeletes local Postgres/Redis/MinIO data; use only for an intentional reset after confirming the compose project name.- Never run recursive container/volume cleanup against all Docker projects on a shared workstation.
- Production migration/backup/restore follows the deployment runbook, not local reset commands.
Use only immutable published image tags:
Copy-Item .env.production.example .env.production
$env:IMAGE_TAG='v4.0.0' # or sha-<full-commit>
docker compose --env-file .env.production \
-f docker-compose.yml -f docker-compose.prod.yml config --quiet
docker compose --env-file .env.production \
-f docker-compose.yml -f docker-compose.prod.yml pull --ignore-buildable
docker compose --env-file .env.production \
-f docker-compose.yml -f docker-compose.prod.yml build postgres
docker compose --env-file .env.production \
-f docker-compose.yml -f docker-compose.prod.yml up -dThis profile uses Socket.IO/Redis/MinIO by design. Its PostGIS + pgvector postgres service is build-only local infrastructure, not a published release image: run pull --ignore-buildable, then build postgres, before up. It is not a fallback for a misconfigured Supabase/Railway/Vercel deployment.
- Wrong API/CORS: inspect the web image's baked
NEXT_PUBLIC_API_URLand use a configured origin (localhostversus127.0.0.1matters). - Migration blocks API: inspect
migratelogs and database URL; do not bypassdepends_onor mark migration successful manually. - Worker/RAG is stale: inspect
workerlogs forRAG sync scheduled,RAG sync complete, or a reported provider/source failure. Do not replace the worker with an API-side background loop. - Sharp/native load failure: rebuild both requested platforms; do not mix Alpine/musl build output with Debian/glibc distroless runtime.
- Redis unhealthy: BullMQ requires
maxmemory-policy noeviction. - Maps unavailable: configure a browser-restricted key and verify real route telemetry; do not add hardcoded coordinates.
- AI unconfigured: rotate/add the server key through a secret manager; do not add a fallback response that pretends to be an LLM.
- Remote CI unavailable: continue bounded local work and record evidence, but do not deploy, merge to
master, or publish release tags until CI is restored.