diff --git a/.gitignore b/.gitignore index ee3f08a..d5c6f51 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,11 @@ build/ .env.local .env.*.local .env.secrets +.env.production +infrastructure/.env.production !.env.development !.env.example +!.env.production.example !.env.secrets.example # IDE diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..2784b39 --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,34 @@ +# Cards-API: Hono + Bun, single-stage Image (Bun bundled keine +# Native-Modules, kein separates Build-Stage nötig). +# +# Wird vom infrastructure/docker-compose.production.yml gebaut, daher +# der Build-Context = Repo-Root (../ aus apps/api). + +FROM oven/bun:1.1-alpine + +WORKDIR /app + +# Lockfile + workspace-Skelett zuerst, damit bun den Layer cached. +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ +COPY apps/api/package.json apps/api/ +COPY packages/cards-domain/package.json packages/cards-domain/ + +# Verdaccio-Auth via Build-Arg → .npmrc. +ARG NPM_AUTH_TOKEN +ENV NPM_AUTH_TOKEN=${NPM_AUTH_TOKEN} +COPY .npmrc ./ + +# pnpm via corepack — Bun-Image hat es nicht out-of-the-box. +RUN apk add --no-cache nodejs npm bash \ + && npm install -g pnpm@9.15.9 \ + && pnpm install --frozen-lockfile --prod=false + +# Domain-Paket + API-Source einkopieren. +COPY packages/cards-domain packages/cards-domain +COPY apps/api apps/api + +WORKDIR /app/apps/api + +EXPOSE 3081 + +CMD ["bun", "run", "src/index.ts"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..055f477 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,44 @@ +# Cards-Web: SvelteKit (adapter-node), 2-stage build. +# Build-Context = Repo-Root. + +FROM node:20-alpine AS builder + +WORKDIR /app + +ARG NPM_AUTH_TOKEN +ENV NPM_AUTH_TOKEN=${NPM_AUTH_TOKEN} + +ARG PUBLIC_CARDS_API_URL +ENV PUBLIC_CARDS_API_URL=${PUBLIC_CARDS_API_URL} + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json .npmrc ./ +COPY apps/web/package.json apps/web/ +COPY packages/cards-domain/package.json packages/cards-domain/ + +RUN npm install -g pnpm@9.15.9 \ + && pnpm install --frozen-lockfile --prod=false + +COPY packages/cards-domain packages/cards-domain +COPY apps/web apps/web + +WORKDIR /app/apps/web +RUN pnpm exec svelte-kit sync && pnpm exec vite build + +# --- runtime stage --- +FROM node:20-alpine AS runtime + +WORKDIR /app + +# adapter-node bundled das complete server-bundle in build/, sonst nichts. +COPY --from=builder /app/apps/web/build /app/build +COPY --from=builder /app/apps/web/package.json /app/package.json + +# adapter-node hat keine externen Deps zur Laufzeit (alles ist in build/ +# gebundelt), wir starten direkt mit node. +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOST=0.0.0.0 + +EXPOSE 3000 + +CMD ["node", "build/index.js"] diff --git a/infrastructure/.env.production.example b/infrastructure/.env.production.example new file mode 100644 index 0000000..d3f42aa --- /dev/null +++ b/infrastructure/.env.production.example @@ -0,0 +1,15 @@ +# Cards-Production-Env-Skeleton. Auf dem Mac Mini liegt die echte +# `.env.production` mit den richtigen Secrets — diese Datei ist nur +# das Template (committet, ohne Secrets). +# +# Generiere die Secrets z.B. mit `openssl rand -hex 32`. + +CARDS_DB_PASSWORD=change-me-strong-random +CARDS_S3_SECRET_KEY=change-me-32-bytes-base64 +CARDS_DSGVO_SERVICE_KEY=change-me-msk-prefix +CARDS_API_VERSION=1.0.0 + +# Verdaccio-Token für @mana/* — nutze denselben claudebot-Token, den +# auch die Plattform verwendet (siehe ~/.cloudflared/.npmrc oder +# secret_verdaccio_claudebot.md). +NPM_AUTH_TOKEN= diff --git a/infrastructure/docker-compose.production.yml b/infrastructure/docker-compose.production.yml new file mode 100644 index 0000000..fcbe0dd --- /dev/null +++ b/infrastructure/docker-compose.production.yml @@ -0,0 +1,107 @@ +# Production-Stack für Cards auf dem Mac Mini. +# +# Lebt unter ~/projects/cards/ auf mana-server (Forgejo-Klon von +# git.mana.how/till/cards). Build-Contexte zeigen relativ in den +# Repo, kein externes Image-Registry — Cards ist Greenfield-eigenständig +# (Strategie B), kein Plattform-Coupling. +# +# Ports auf dem Mac Mini: +# cards-postgres: 5436 (Plattform 5432, Dev 5435 sind belegt) +# cards-minio S3: 9110 +# cards-minio UI: 9111 +# cards-api: 3091 (alt war 3072 → cards-api.mana.how) +# cards-web: 5181 (alt war 5180 → cards.mana.how) +# +# Cutover (Hard) auf cards.* / cards-api.* — siehe +# scripts/mac-mini-cutover.sh, der den Tunnel umbiegt + alte +# mana-app-cards-{server,web}-Container stoppt. +# +# Start (von ~/projects/cards/ auf mana-server): +# docker compose -f infrastructure/docker-compose.production.yml \ +# --env-file infrastructure/.env.production up -d --build +# +# Stop: +# docker compose -f infrastructure/docker-compose.production.yml down + +services: + cards-postgres: + image: postgres:16-alpine + container_name: cards-postgres + restart: unless-stopped + environment: + POSTGRES_USER: cards + POSTGRES_PASSWORD: ${CARDS_DB_PASSWORD:?missing CARDS_DB_PASSWORD} + POSTGRES_DB: cards + ports: + - '127.0.0.1:5436:5432' + volumes: + - /Volumes/ManaData/cards/postgres:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U cards -d cards'] + interval: 5s + timeout: 3s + retries: 20 + + cards-minio: + image: minio/minio:latest + container_name: cards-minio + restart: unless-stopped + command: server /data --console-address ':9001' + environment: + MINIO_ROOT_USER: cardsadmin + MINIO_ROOT_PASSWORD: ${CARDS_S3_SECRET_KEY:?missing CARDS_S3_SECRET_KEY} + ports: + - '127.0.0.1:9110:9000' + - '127.0.0.1:9111:9001' + volumes: + - /Volumes/ManaData/cards/minio:/data + healthcheck: + test: ['CMD', 'mc', 'ready', 'local'] + interval: 5s + timeout: 3s + retries: 10 + + cards-api: + image: cards-api:local + container_name: cards-api + build: + context: ../ + dockerfile: apps/api/Dockerfile + restart: unless-stopped + depends_on: + cards-postgres: + condition: service_healthy + cards-minio: + condition: service_healthy + environment: + DATABASE_URL: 'postgresql://cards:${CARDS_DB_PASSWORD}@cards-postgres:5432/cards' + CARDS_API_PORT: 3081 + CARDS_API_VERSION: ${CARDS_API_VERSION:-1.0.0} + CARDS_PUBLIC_URL: https://cards.mana.how + CARDS_DSGVO_SERVICE_KEY: ${CARDS_DSGVO_SERVICE_KEY:?missing CARDS_DSGVO_SERVICE_KEY} + CARDS_S3_ENDPOINT: cards-minio + CARDS_S3_PORT: 9000 + CARDS_S3_USE_SSL: 'false' + CARDS_S3_ACCESS_KEY: cardsadmin + CARDS_S3_SECRET_KEY: ${CARDS_S3_SECRET_KEY} + CARDS_S3_BUCKET: cards-media + ports: + - '127.0.0.1:3091:3081' + + cards-web: + image: cards-web:local + container_name: cards-web + build: + context: ../ + dockerfile: apps/web/Dockerfile + args: + NPM_AUTH_TOKEN: ${NPM_AUTH_TOKEN:?missing NPM_AUTH_TOKEN} + PUBLIC_CARDS_API_URL: https://cards-api.mana.how + restart: unless-stopped + depends_on: + - cards-api + environment: + CARDS_API_URL: https://cards-api.mana.how + NODE_ENV: production + ports: + - '127.0.0.1:5181:3000'