ByteBite Docs
Deployment

Local Deployment

Docker-based local development infrastructure and setup.

Local Deployment

ByteBite local development runs entirely in Docker with Traefik for hostname-based routing. No Dokploy or TLS is involved.

Prerequisites

  • macOS (primary target) or Linux
  • Docker Desktop or compatible runtime (Compose v2+)
  • Git, pnpm, Node.js 24 LTS (for host-side lint/typecheck/build)
  • /etc/hosts entries (see below)

Architecture

Browser -> Traefik :80 (compose.yml)
  -> file provider (infrastructure/traefik/dev/dynamic.yml)
    -> landing | storefront | dashboard | api | docs
      -> PostgreSQL (backend network)
      -> MinIO (backend network, API only)

Traefik uses the file provider only — Docker provider is disabled for OrbStack compatibility. Service traefik.* labels in compose.yml are documentation only.

See Network for Docker network details and Architecture for request flows.

Compose files

FilePurpose
compose.ymlBase services, networks, Traefik, PostgreSQL, MinIO
compose.dev.ymlDev overrides: bind mounts, hot reload commands

Start with:

pnpm dev    # docker compose -f compose.yml -f compose.dev.yml up

Local hostnames

Domain suffix: .bytebite.test

HostServicePort (internal)
testlinks.bytebite.testStatic local link page (nginx)80
storefront.bytebite.testStorefront (neutral dev host)3000
dashboard.bytebite.testDashboard (Vite dev)5173
api.bytebite.testAPI (direct)3001
docs.bytebite.testDocs3002
landing.bytebite.testLanding (Coming Soon)3003
luigi.bytebite.testStorefront (seeded tenant)3000
mario.bytebite.testStorefront (seeded tenant)3000
unknown.bytebite.testStorefront (unknown tenant test)3000

No wildcard DNS — each host must be listed in /etc/hosts.

/etc/hosts — IPv4 and IPv6 required

macOS clients may attempt AAAA (IPv6) lookups for .test hostnames. If only 127.0.0.1 entries exist, requests can hang for ~5 seconds before falling back to IPv4.

Add both 127.0.0.1 and ::1 for every host:

127.0.0.1 testlinks.bytebite.test
127.0.0.1 storefront.bytebite.test
127.0.0.1 dashboard.bytebite.test
127.0.0.1 api.bytebite.test
127.0.0.1 docs.bytebite.test
127.0.0.1 landing.bytebite.test
127.0.0.1 luigi.bytebite.test
127.0.0.1 mario.bytebite.test
127.0.0.1 unknown.bytebite.test
::1 testlinks.bytebite.test
::1 storefront.bytebite.test
::1 dashboard.bytebite.test
::1 api.bytebite.test
::1 docs.bytebite.test
::1 landing.bytebite.test
::1 luigi.bytebite.test
::1 mario.bytebite.test
::1 unknown.bytebite.test

See root README.md for a convenient macOS one-liner.

Same-origin /api routing

Browsers call /api/* on the same host as the UI. Traefik strips the /api prefix before forwarding to Fastify:

http://luigi.bytebite.test/api/health
  -> Traefik (strip-api-prefix middleware)
    -> api:3001 /health

Configured in infrastructure/traefik/dev/dynamic.yml. No CORS layer is needed for the normal browser path.

API_INTERNAL_URL

Storefront SSR uses API_INTERNAL_URL=http://api:3001 (set in compose.yml). Server-side fetches send X-ByteBite-Tenant-Host with the original browser hostname. See Architecture.

PostgreSQL and MinIO

ServiceAccess
PostgreSQL127.0.0.1:5432 (host-published for tooling)
MinIO consolehttp://localhost:9001
Traefik dashboardhttp://localhost:8080
Drizzle Studiohttp://localhost:4983 (pnpm db:studio)

Inside the API container, Compose overrides DATABASE_URL to postgres://...@postgres:5432/... and S3_ENDPOINT to http://minio:9000.

Migrations and seeds

The API does not auto-migrate. With the stack running:

pnpm db:migrate
pnpm db:seed

Local seeds create luigi.bytebite.test and mario.bytebite.test, plus demo staff users restaurant@bytebite.test (RESTAURANT) and admin@bytebite.test (ADMIN). Passwords are seed/test secrets only (restaurant-seed / admin-seed). Seeds do not create carts or orders. Staff login is http://dashboard.bytebite.test/login. Do not run pnpm db:seed:staging locally — it writes staging hostnames.

BETTER_AUTH_SECRET is wired for staff login (Better Auth). The local placeholder (change-me-to-a-long-random-string) is not a production secret. Production/staging must set a long random secret; without it the API does not register /auth routes (no silent Better Auth default) and /dashboard/* returns 401. session.restaurantId is not this slice — dashboard mutations stay on the path. Dashboard API calls need the session cookie; Traefik BasicAuth is not a substitute.

Smoke tests

curl -s "http://api.bytebite.test/health"
curl -s "http://luigi.bytebite.test/api/health"
curl -s "http://luigi.bytebite.test/api/storefront/restaurant"
curl -s "http://luigi.bytebite.test/api/storefront/hours"
curl -s "http://luigi.bytebite.test/api/storefront/menu"
curl -s "http://luigi.bytebite.test/api/storefront/cart"
curl -s "http://mario.bytebite.test/api/storefront/menu"
curl -s "http://mario.bytebite.test/api/storefront/cart"
curl -s "http://unknown.bytebite.test/api/storefront/restaurant" -o /dev/null -w "%{http_code}"
curl -s "http://unknown.bytebite.test/api/storefront/menu" -o /dev/null -w "%{http_code}"
curl -s "http://unknown.bytebite.test/api/storefront/cart" -o /dev/null -w "%{http_code}"
curl -s "http://unknown.bytebite.test/api/storefront/checkout" -o /dev/null -w "%{http_code}"
curl -s "http://unknown.bytebite.test/api/storefront/hours" -o /dev/null -w "%{http_code}"

Unknown-host restaurant, menu, cart, checkout, and hours routes return 404. Luigi's menu includes Margherita and excludes Carbonara; Mario is the inverse. Empty carts are 200 with items: []. A Luigi bb_cart cookie sent to Mario yields a new empty Mario cart and cannot create a Luigi order. Body restaurantId does not change the tenant. A Luigi orderId on Mario returns 404. Dashboard hours and logo UI: http://dashboard.bytebite.test/. Dashboard menu UI (including category/item image upload): http://dashboard.bytebite.test/menu. Dashboard accept/reject and kitchen UI: http://dashboard.bytebite.test/orders. Pickup checkout is 409 when the hostname restaurant is closed; cart/menu GETs stay 200.

Checkout isolation after seed (no orders are seeded):

curl -s -D - -H "content-type: application/json" \
  -d '{"menuItemId":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa5","quantity":2}' \
  "http://luigi.bytebite.test/api/storefront/cart/items"
curl -s -H "content-type: application/json" \
  -H "cookie: bb_cart=<luigi-cart-uuid>" \
  -d '{"name":"Anna","phone":"+491111111111","restaurantId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb1"}' \
  "http://luigi.bytebite.test/api/storefront/checkout"
curl -s -o /dev/null -w "%{http_code}" \
  "http://mario.bytebite.test/api/storefront/orders/<luigi-order-uuid>"

Luigi snapshots Margherita at 850 / 1700. Mario returns 404 for that orderId.

Accept / reject / timeout (after checkout; seeds still create no orders). Dashboard writes need a staff session (not Traefik BasicAuth):

LUIGI_ID="aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1"
MARIO_ID="bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb1"
curl -s -c /tmp/bb-staff.txt -H "content-type: application/json" \
  -H "origin: http://dashboard.bytebite.test" \
  -d '{"email":"restaurant@bytebite.test","password":"restaurant-seed"}' \
  "http://dashboard.bytebite.test/api/auth/sign-in/email"
curl -s -b /tmp/bb-staff.txt -o /dev/null -w "%{http_code}" \
  "http://dashboard.bytebite.test/api/dashboard/restaurants/${LUIGI_ID}/menu"
# Expected without cookie: 401. Expected with cookie: 200.
curl -s -b /tmp/bb-staff.txt -H "content-type: application/json" \
  -d '{"etaMinutes":30}' \
  "http://dashboard.bytebite.test/api/dashboard/restaurants/${LUIGI_ID}/orders/<luigi-order-uuid>/accept"
curl -s -o /dev/null -w "%{http_code}" -b /tmp/bb-staff.txt \
  -H "content-type: application/json" \
  -d '{"etaMinutes":30}' \
  "http://dashboard.bytebite.test/api/dashboard/restaurants/${MARIO_ID}/orders/<luigi-order-uuid>/accept"

Luigi accept returns accepted with ETA 30. The same orderId under Mario's path returns 404 and does not write. Dashboard menu without a session is 401. Staff login/logout: http://dashboard.bytebite.test/login.

Kitchen after accept (seeds still create no orders):

curl -s -b /tmp/bb-staff.txt -H "content-type: application/json" \
  -d '{}' \
  "http://dashboard.bytebite.test/api/dashboard/restaurants/${LUIGI_ID}/orders/<luigi-order-uuid>/preparing"
curl -s -b /tmp/bb-staff.txt -H "content-type: application/json" \
  -d '{}' \
  "http://dashboard.bytebite.test/api/dashboard/restaurants/${LUIGI_ID}/orders/<luigi-order-uuid>/ready"
curl -s -b /tmp/bb-staff.txt -H "content-type: application/json" \
  -d '{}' \
  "http://dashboard.bytebite.test/api/dashboard/restaurants/${LUIGI_ID}/orders/<luigi-order-uuid>/complete"
curl -s -o /dev/null -w "%{http_code}" -b /tmp/bb-staff.txt \
  -H "content-type: application/json" \
  -d '{}' \
  "http://dashboard.bytebite.test/api/dashboard/restaurants/${MARIO_ID}/orders/<luigi-order-uuid>/preparing"

Luigi kitchen writes return preparing, then ready, then completed (ETA 30 and snapshot prices unchanged). The same orderId under Mario's path returns 404 and does not write.

Logo upload (multipart file; seeds still create no uploads):

LUIGI_ID="aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1"
MARIO_ID="bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb1"
curl -s -b /tmp/bb-staff.txt -F "file=@./logo.png;type=image/png" \
  "http://dashboard.bytebite.test/api/dashboard/restaurants/${LUIGI_ID}/logo"
curl -s "http://luigi.bytebite.test/api/storefront/restaurant"
curl -s "http://mario.bytebite.test/api/storefront/restaurant"
curl -s -o /dev/null -w "%{http_code}" -b /tmp/bb-staff.txt -F "file=@./logo.png;type=image/png" \
  "http://dashboard.bytebite.test/api/dashboard/restaurants/${LUIGI_ID}/items/${MARIO_ID}/image"

Luigi logoUrl is a /api/media/restaurants/${LUIGI_ID}/... path. Mario's restaurant payload still has logoUrl: null. A Mario id in Luigi's item-image path returns 404.

More checks: Validation.

Testing deployment compose locally

Use a separate project name to avoid replacing dev containers:

docker network create dokploy-network
docker compose -p bytebite-staging -f compose.deploy.yml up --build

Dev images are tagged bytebite-<service>:dev and deploy images bytebite-<service>:prod, so a missing -p no longer overwrites the images pnpm dev starts. Still use -p bytebite-staging so deploy does not replace running dev containers.

dokploy-network is declared external: true in compose.deploy.yml (required by the migrate one-shot on staging). Create it first or compose fails. This local run still does not include Traefik, PostgreSQL, or local routing; migrate / api need a reachable DATABASE_URL.

Troubleshooting

IssueFix
~5s request delay on macOSAdd ::1 entries to /etc/hosts
api.bytebite.test connection refusedEnsure pnpm dev is running
Cannot find module '/app/pnpm'Dev stack started production images. pnpm dev:down then pnpm dev (builds :dev tags).
Dashboard pnpm: not found (exit 127)Same cause as above — dashboard prod is nginx, which has no pnpm
New docs page not visibleRestart docs service after adding MDX files
Host-side build corrupts Next dev cacheAnonymous volumes protect .next — avoid host pnpm build for storefront/docs during dev

See Troubleshooting for infrastructure issues.

On this page