From 7750c46a12120ee20c16e4a105b5ab0c3803f3e9 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 9 Apr 2026 14:08:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(api):=20production=20deploy=20=E2=80=94=20?= =?UTF-8?q?Dockerfile=20+=20docker-compose=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the missing production deployment artifacts for the unified apps/api Hono/Bun server. Until now apps/api was code-only — built during the consolidation sweep but never wired into the Mac Mini compose stack, so all 17 product modules that depend on it (calendar, todo, picture, planta, nutriphi, news, traces, presi, music, contacts, storage, context, guides, research, chat, moodlit, who) effectively had no backend in production. The frontend modules shipped, but their compute calls fell through to localhost:3060 in the browser and just failed. This commit fixes the gap. apps/api/Dockerfile (NEW) ------------------------- Multi-stage Bun build that runs from the monorepo root so the four workspace dependencies (@mana/shared-hono, @mana/shared-logger, @mana/shared-storage, @mana/media-client) actually resolve. Builder stage installs via pnpm with the --filter @mana/api... selector to keep the install graph minimal; runtime stage copies the resulting workspace tree (including the pnpm symlink farm) and runs the entry script with bun directly — no compile step, since bun handles TypeScript natively. @mana/media-client lives under services/mana-media/packages/client, not packages/, so the COPY path is the awkward services/mana-media/packages/client → ./services/mana-media/packages/ client mirror to keep the workspace layout intact. Healthcheck hits /health every 30s with a 15s start period — same shape as the other Bun services in this compose file. docker-compose.macmini.yml — new mana-api service ------------------------------------------------- Slotted between glitchtip-worker and the games section. Build context is the monorepo root (`.`) because the Dockerfile needs the workspace tree. Container name `mana-api`, image `mana-api:local`, mem_limit 384m (higher than the smaller Bun services because the unified server holds 17 modules' route definitions + Drizzle schema caches in memory). Environment wires up everything apps/api needs: - MANA_AUTH_URL → mana-auth:3001 for JWT validation - MANA_LLM_URL → mana-llm:3025 for chat / picture / who LLM calls - MANA_SEARCH_URL → mana-search:3012 for guides / research - MANA_CREDITS_URL → mana-credits:3002 for credit validation - MANA_MEDIA_URL → mana-media:3011 for image uploads - DATABASE_URL → mana_platform Postgres for the few server-side state stores (research_results, presi share-links, traces guides) - MANA_SERVICE_KEY → for the credit/auth service-to-service calls - LOGGER_FORMAT=json → structured logs for grafana ingestion - CORS_ORIGINS=https://mana.how → only the unified web origin needs access, the standalone game frontends don't call this Port 3060 is exposed on the host so cloudflared can route api.mana.how → mana-api:3060 (separate Mac Mini side step, not in this commit). docker-compose.macmini.yml — mana-web wiring -------------------------------------------- Two new env vars: PUBLIC_MANA_API_URL=http://mana-api:3060 PUBLIC_MANA_API_URL_CLIENT=https://api.mana.how The hooks.server.ts injection plumbing for window.__PUBLIC_MANA_API_URL__ already existed (added in an earlier sweep but never had a value to inject). The CSP connect-src list and the SSR injection script tag also already include PUBLIC_MANA_API_URL_CLIENT — so once the env arrives, the existing client-side getManaApiUrl() helper picks it up automatically. mana-web also gets a depends_on entry on mana-api with condition: service_healthy so the web container doesn't start serving requests against a dead API. Verification ------------ docker compose -f docker-compose.macmini.yml config validates cleanly (no YAML errors). Image build is NOT exercised in this commit — that happens on the Mac Mini via build-app.sh after the push lands. Out of scope for this commit (Mac Mini side, manual steps): 1. ssh mana-server, git pull 2. ./scripts/mac-mini/build-app.sh mana-api (first build, ~3-5 min) 3. ./scripts/mac-mini/build-app.sh mana-web (rebuild with new env) 4. cloudflared route: add api.mana.how → mana-api:3060 to ~/.cloudflared/config.yml and `systemctl restart cloudflared` 5. Test https://api.mana.how/health from anywhere 6. Test https://mana.how/who in a browser Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/Dockerfile | 61 ++++++++++++++++++++++++++++++++++++ docker-compose.macmini.yml | 64 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 apps/api/Dockerfile diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 000000000..cae8800a6 --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,61 @@ +# syntax=docker/dockerfile:1 +# +# apps/api — unified Hono/Bun API server +# +# Multi-stage build that runs from the monorepo root so the workspace +# packages (@mana/shared-hono, @mana/shared-storage, @mana/media-client, +# @mana/shared-logger) resolve via pnpm before being copied into a +# minimal runtime image. +# +# Build context MUST be the monorepo root, not apps/api/. The compose +# service uses `context: .` for this reason. + +FROM oven/bun:1 AS builder + +WORKDIR /app + +# Copy the workspace manifest first so the dependency graph is known +# before we add source. This caches the install layer for incremental +# rebuilds when only source changes. +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY apps/api/package.json ./apps/api/package.json + +# Workspace packages that apps/api depends on, plus their transitive +# workspace deps. Listed explicitly so the install layer doesn't pull +# in the entire monorepo. +COPY packages/shared-hono ./packages/shared-hono +COPY packages/shared-logger ./packages/shared-logger +COPY packages/shared-storage ./packages/shared-storage +# @mana/media-client lives under services/mana-media (sub-package). +COPY services/mana-media/packages/client ./services/mana-media/packages/client + +# Install pnpm and resolve the dependency graph for apps/api. +RUN npm install -g pnpm@9.15.0 +RUN pnpm install --filter @mana/api... --no-frozen-lockfile --ignore-scripts + +# Copy the api source and tsconfig last so source-only changes don't +# bust the install cache. +COPY apps/api/src ./apps/api/src +COPY apps/api/tsconfig.json ./apps/api/tsconfig.json + + +# ─── Runtime stage ───────────────────────────────────────────── +# +# Bun can run TypeScript directly without a compile step, so the +# runtime image just needs the workspace tree the builder produced. +# We copy /app wholesale rather than try to slice node_modules — the +# pnpm symlink farm is fragile and easy to break with selective copies. + +FROM oven/bun:1 AS production + +WORKDIR /app +COPY --from=builder /app /app + +WORKDIR /app/apps/api + +EXPOSE 3060 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD bun -e "fetch('http://localhost:3060/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +CMD ["bun", "run", "src/index.ts"] diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index 3e3f20dcb..ebc5ab717 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -746,6 +746,8 @@ services: depends_on: mana-auth: condition: service_healthy + mana-api: + condition: service_healthy environment: NODE_ENV: production PORT: 5000 @@ -753,6 +755,13 @@ services: PUBLIC_MANA_AUTH_URL_CLIENT: https://auth.mana.how PUBLIC_SYNC_SERVER_URL: http://mana-sync:3010 PUBLIC_SYNC_SERVER_URL_CLIENT: https://sync.mana.how + # Unified Hono/Bun API server (apps/api) — hosts all 16 product + # compute modules (calendar, todo, picture, … who) on port 3060. + # Browser calls go through https://api.mana.how (cloudflared + # tunnel route to mana-api:3060). SSR calls inside the docker + # network use the internal hostname. + PUBLIC_MANA_API_URL: http://mana-api:3060 + PUBLIC_MANA_API_URL_CLIENT: https://api.mana.how # Per-app HTTP backend URLs (todo-api, calendar-api, contacts-api, # chat-api, storage-api, cards-api, music-api, nutriphi-api, # picture-api, presi-api, zitare-api, clock-api, context-api) and @@ -1441,6 +1450,61 @@ services: redis: condition: service_healthy + # ============================================ + # Unified API Server + # ============================================ + # apps/api — Hono/Bun process that hosts all 16 product compute + # modules (calendar, todo, chat, picture, planta, nutriphi, news, + # traces, moodlit, presi, music, contacts, storage, context, guides, + # research, who) on a single port. Replaces ~17 per-product backend + # containers from the pre-consolidation era; the unified Mana web + # app's compute calls all flow through here. + + mana-api: + build: + context: . + dockerfile: apps/api/Dockerfile + image: mana-api:local + container_name: mana-api + restart: always + mem_limit: 384m + depends_on: + postgres: + condition: service_healthy + mana-auth: + condition: service_healthy + environment: + TZ: Europe/Berlin + NODE_ENV: production + PORT: 3060 + # Auth (JWT validation via JWKS) + MANA_AUTH_URL: http://mana-auth:3001 + JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY:-} + # Compute services apps/api orchestrates + MANA_LLM_URL: http://mana-llm:3025 + MANA_SEARCH_URL: http://mana-search:3012 + MANA_CREDITS_URL: http://mana-credits:3002 + MANA_MEDIA_URL: http://mana-media:3011 + MANA_SERVICE_KEY: ${MANA_SERVICE_KEY} + APP_ID: mana-api + # Database (used by modules that have server-side state — research, + # presi share-links, traces guides). Same Postgres + schema split + # as the rest of the platform. + DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:-mana123}@postgres:5432/mana_platform + # CORS — only the unified mana.how origin needs access today. + # The arcade + manavoxel game frontends don't call apps/api. + CORS_ORIGINS: https://mana.how + # Structured-logger format + LOGGER_FORMAT: json + ports: + - "3060:3060" + healthcheck: + test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:3060/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 60s + timeout: 5s + retries: 3 + start_period: 30s + # ============================================ # Games # ============================================