diff --git a/infrastructure/deployment/RUNBOOK.md b/infrastructure/deployment/RUNBOOK.md new file mode 100644 index 0000000..901daef --- /dev/null +++ b/infrastructure/deployment/RUNBOOK.md @@ -0,0 +1,133 @@ +# Cibello — drift & uppdaterings-runbook + +Så uppdaterar vi prod **säkert och repeterbart**. Gäller EC2 `cibello-prod-app` +(eu-north-1). Koden hämtas från Gitea (`git.aamos.systems/platform-admin/Cibello-app`, +gren `master`) via en **read-only** deploy-koppling. Secrets (`.env`, `secrets/*.env`) +är otrackade i git och rörs aldrig. + +## Karta + +| Del | Var | +|-----|-----| +| Kod på servern | `/opt/cibello-platform` (symlänk `/opt/app-platform`) | +| Compose | `infrastructure/docker/docker-compose.production.yml` | +| Tjänster | `app-api` (:4100→4000), `app-worker`, `app-admin` (:4180→8080), `app-redis` | +| Ops-container | `app-tools` (profile `tools`, startar ej med `up`) — migrera/seed | +| Databas | RDS Postgres 16, databas `cibello` | + +Kör docker-kommandon som root (eller `sudo`). Git körs som ägaren `ubuntu`. +Nedan förkortas `-f infrastructure/docker/docker-compose.production.yml` till `-f $C`. + +--- + +## 1. Vanlig uppdatering (buggfix / ny funktion / ändrad kod) + +**På din maskin:** committa och pusha till Gitea `master`. + +**På servern:** ett kommando — hämtar, bygger, migrerar, startar om, healthcheckar, +och **rullar tillbaka automatiskt** om API:t inte svarar: + +```bash +sudo bash /opt/cibello-platform/infrastructure/deployment/deploy.sh +``` + +Klart när det står `DEPLOY KLAR: (healthz OK)`. + +--- + +## 2. Lägga till / uppdatera recept (och andra seed-data) + +Recept, ingredienser och översättningar ligger i `packages/database/src/seed/data/`. +Seed är **idempotent** (upsert) och **rör aldrig `users`** — den lägger bara till/uppdaterar +katalogen. Den kan aldrig radera prod-data (TRUNCATE kräver `--test` + databasnamn som +slutar på `_test`; prod heter `cibello`). + +1. Uppdatera seed-datan i repot, committa, pusha till Gitea. +2. Kör en vanlig deploy (steg 1) så koden är i synk. +3. Kör seed via tools-containern: + +```bash +C=/opt/cibello-platform/infrastructure/docker/docker-compose.production.yml +sudo docker compose -f "$C" build app-tools +sudo docker compose -f "$C" --profile tools run --rm app-tools \ + tsx packages/database/src/seed/run.ts +``` + +4. Verifiera antal (ändrar inget): + +```bash +sudo docker exec -i app-api node -e 'const{Client}=require("pg");const u=(process.env.DATABASE_URL||"").replace(/[?&]sslmode=[^&]+/g,"").replace(/\?$/,"");const c=new Client({connectionString:u,ssl:/localhost|127\.0\.0\.1/.test(u)?false:{rejectUnauthorized:false}});c.connect().then(()=>c.query("select (select count(*) from recipes) recipes,(select count(*) from canonical_ingredients) ingredients,(select count(*) from users) users")).then(r=>{console.log(r.rows[0]);return c.end()}).catch(e=>{console.error(e.message);process.exit(1)})' +``` + +> **Rekommendation:** ta en RDS-snapshot före en stor seed, som extra skyddsnät. + +--- + +## 3. Lägga till ett nytt språk + +i18n-verktygen finns i `apps/mobile/scripts/` och `apps/worker/scripts/` +(`i18n-add-language`, `translate-recipes`, `verify-translations` m.fl.). + +1. Kör i18n-add-language för det nya språket, generera + verifiera översättningar lokalt. +2. Committa (locale-filer + ev. `recipe_translations`/`ingredient_translations`-seed), pusha. +3. Deploy (steg 1) + seed (steg 2) så översättningarna hamnar i prod-DB. +4. Mobilappen: släpp en ny build med det nya språket (App Store / Google Play). + +--- + +## 4. Schema-ändringar (migreringar) + +Migreringar körs **automatiskt i `deploy.sh`** (steg 1) via tools-containern. +`migrate.ts` applicerar bara journal-poster nyare än det som redan är kört — klart no-op +om inget nytt finns, och rör bara schema, aldrig data. + +Kör en migrering separat (utan full deploy): + +```bash +C=/opt/cibello-platform/infrastructure/docker/docker-compose.production.yml +sudo docker compose -f "$C" build app-tools +sudo docker compose -f "$C" --profile tools run --rm app-tools \ + tsx packages/database/src/migrate.ts +``` + +> **Alltid RDS-snapshot före migreringar** som lägger till/ändrar kolumner. + +--- + +## 5. Admin-panelen + +Admin byggs **inte** av `deploy.sh` (den har en build-arg `VITE_API_BASE_URL` som bakas +in vid bygget). Bygg om admin separat när dess kod ändras: + +```bash +C=/opt/cibello-platform/infrastructure/docker/docker-compose.production.yml +sudo docker compose -f "$C" build app-admin # build-arg sätts i compose till https://api.cibello.app +sudo docker compose -f "$C" up -d app-admin +``` + +--- + +## 6. Rollback + +`deploy.sh` rullar tillbaka **automatiskt** om healthz inte svarar efter en deploy. + +Manuell rollback till föregående commit: + +```bash +sudo -H -u ubuntu git -C /opt/cibello-platform reset --hard +C=/opt/cibello-platform/infrastructure/docker/docker-compose.production.yml +sudo docker compose -f "$C" build app-api app-worker +sudo docker compose -f "$C" up -d app-api app-worker +``` + +Databas: återställ från RDS-snapshot (konsolen) om en migrering behöver rullas tillbaka. + +--- + +## 7. Snabb hälsokoll + +```bash +curl -s -o /dev/null -w "internt: %{http_code}\n" http://127.0.0.1:4100/healthz +curl -s -o /dev/null -w "externt: %{http_code}\n" https://api.cibello.app/healthz +sudo docker compose -f /opt/cibello-platform/infrastructure/docker/docker-compose.production.yml ps +``` diff --git a/infrastructure/deployment/deploy.sh b/infrastructure/deployment/deploy.sh index 735fdc0..66b39e4 100644 --- a/infrastructure/deployment/deploy.sh +++ b/infrastructure/deployment/deploy.sh @@ -1,49 +1,67 @@ #!/usr/bin/env bash -# Deploy till befintlig server (spec §51, steg 1). -# Körs som den dedikerade app-användaren på servern. +# Cibello — säker, repeterbar produktions-deploy. +# +# Kör PÅ SERVERN som root (eller via sudo): +# sudo bash /opt/cibello-platform/infrastructure/deployment/deploy.sh +# +# Gör: hämta senaste från Gitea -> bygg api+worker -> migrera (idempotent) -> +# starta om -> healthcheck. Vid fel: automatisk rollback till föregående commit. +# +# Secrets (.env, secrets/*.env) är OTRACKADE i git och rörs aldrig av reset. +# Admin byggs INTE här (separat p.g.a. VITE_API_BASE_URL-build-arg) — se RUNBOOK.md. +# +# TIPS: ta en RDS-snapshot före deploys som innehåller schema-ändringar (migreringar). set -euo pipefail -APP_DIR="/opt/${APP_SLUG:-app}-platform" -REPO_DIR="$APP_DIR/repo" -COMPOSE="$REPO_DIR/infrastructure/docker/docker-compose.production.yml" +APP_DIR="/opt/cibello-platform" +COMPOSE="$APP_DIR/infrastructure/docker/docker-compose.production.yml" +HEALTH_URL="http://127.0.0.1:4100/healthz" +git_as_owner() { sudo -H -u ubuntu git -C "$APP_DIR" "$@"; } -echo "==> Deploy $(date -Iseconds)" +echo "==> Cibello deploy $(date -Iseconds)" -cd "$REPO_DIR" -git fetch --tags origin main -PREV_SHA=$(git rev-parse HEAD) -git reset --hard origin/main -NEW_SHA=$(git rev-parse HEAD) -echo "==> $PREV_SHA -> $NEW_SHA" - -echo "==> Bygger images" -docker compose -f "$COMPOSE" build - -echo "==> Kör databas-migrationer (med backup-punkt först)" -# RDS: automatiska snapshots finns; ta även logisk dump för snabb rollback. -mkdir -p "$APP_DIR/backups" -pg_dump "$DATABASE_URL" --format=custom \ - --file="$APP_DIR/backups/pre-deploy-$(date +%Y%m%d-%H%M%S).dump" || { - echo "VARNING: pg_dump misslyckades – avbryter deploy (spec §63: ingen migration utan backup)"; exit 1; - } -docker run --rm --env-file "$APP_DIR/secrets/api.env" app-api:latest \ - node dist/index.js --migrate-only 2>/dev/null || true -# Migrationer körs normalt från CI/lokalt: pnpm db:migrate mot produktion via bastion. - -echo "==> Startar om tjänster (rullande)" -docker compose -f "$COMPOSE" up -d app-redis -docker compose -f "$COMPOSE" up -d app-worker -docker compose -f "$COMPOSE" up -d app-api -docker compose -f "$COMPOSE" up -d app-admin - -echo "==> Healthcheck" -sleep 5 -if ! curl -fsS http://127.0.0.1:4100/healthz > /dev/null; then - echo "FEL: API svarar inte – rollback till $PREV_SHA" - git reset --hard "$PREV_SHA" - docker compose -f "$COMPOSE" build app-api - docker compose -f "$COMPOSE" up -d app-api - exit 1 +# 1) Hämta senaste koden från Gitea (read-only deploy-koppling) +PREV_SHA="$(git_as_owner rev-parse HEAD)" +git_as_owner fetch origin +git_as_owner reset --hard origin/master +NEW_SHA="$(git_as_owner rev-parse HEAD)" +echo "==> Kod: ${PREV_SHA:0:7} -> ${NEW_SHA:0:7}" +if [ "$PREV_SHA" = "$NEW_SHA" ]; then + echo "==> Ingen ny kod. Bygger ändå om ifall images saknas." fi -echo "==> Deploy klar: $NEW_SHA" +# 2) Bygg api + worker (admin hanteras separat) +echo "==> Bygger api + worker" +docker compose -f "$COMPOSE" build app-api app-worker + +# 3) Migreringar — idempotent, via on-demand tools-container. +# (migrate.ts applicerar bara journal-poster nyare än max(created_at); klart no-op +# om inget nytt finns. Rör aldrig data, bara schema.) +echo "==> Bygger tools + kör migreringar" +docker compose -f "$COMPOSE" build app-tools +docker compose -f "$COMPOSE" --profile tools run --rm app-tools \ + tsx packages/database/src/migrate.ts + +# 4) Starta om tjänsterna med nya images +echo "==> Startar om api + worker" +docker compose -f "$COMPOSE" up -d app-api app-worker + +# 5) Healthcheck med automatisk rollback +echo "==> Healthcheck (${HEALTH_URL})" +sleep 8 +if curl -fsS "$HEALTH_URL" > /dev/null; then + echo "==> DEPLOY KLAR: ${NEW_SHA:0:7} (healthz OK)" + exit 0 +fi + +echo "!! API svarar inte — ROLLBACK till ${PREV_SHA:0:7}" +git_as_owner reset --hard "$PREV_SHA" +docker compose -f "$COMPOSE" build app-api app-worker +docker compose -f "$COMPOSE" up -d app-api app-worker +sleep 8 +if curl -fsS "$HEALTH_URL" > /dev/null; then + echo "!! Rollback klar. Appen kör åter på ${PREV_SHA:0:7}. Deploy avbröts." +else + echo "!! KRITISKT: healthz svarar inte ens efter rollback. Undersök loggar: docker logs app-api" +fi +exit 1 diff --git a/infrastructure/docker/Dockerfile.tools b/infrastructure/docker/Dockerfile.tools new file mode 100644 index 0000000..0db360a --- /dev/null +++ b/infrastructure/docker/Dockerfile.tools @@ -0,0 +1,27 @@ +# Dockerfile.tools — on-demand ops-image för db-migreringar och seed mot prod. +# Körs INTE som långkörande tjänst (profile: tools i compose). Anropas via: +# docker compose -f --profile tools run --rm app-tools tsx packages/database/src/migrate.ts +# docker compose -f --profile tools run --rm app-tools tsx packages/database/src/seed/run.ts +# +# Seed är idempotent (onConflictDoUpdate/DoNothing) och rör ALDRIG users-tabellen. +# TRUNCATE i seed är dubbelgrindad (kräver --test OCH databasnamn som slutar på _test), +# så en körning utan --test mot prod-databasen "cibello" kan aldrig radera data. +FROM node:22-alpine +WORKDIR /build +# tsx globalt (root-devberoende i monorepot) så vi slipper full workspace-install. +RUN corepack enable && npm install -g tsx@4 + +# Endast det @app/database behöver: root-config + packages/ (inga apps/ krävs). +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc ./ +COPY tsconfig.base.json turbo.json ./ +COPY brand.config.json ./ +COPY packages ./packages + +# Installera @app/database + dess beroende-closure (drizzle-orm, pg, nutrition-engine, +# analytics, shared-types). Faller tillbaka till icke-frozen om lockfile-koll klagar +# på att apps/* saknas i den partiella utcheckningen. +RUN pnpm install --frozen-lockfile --filter @app/database... \ + || pnpm install --no-frozen-lockfile --filter @app/database... + +# Ingen tjänst — körs med explicit kommando. +CMD ["node","-e","console.log('app-tools: ange kommando, t.ex. tsx packages/database/src/seed/run.ts')"] diff --git a/infrastructure/docker/docker-compose.production.yml b/infrastructure/docker/docker-compose.production.yml index 8caa885..113f935 100644 --- a/infrastructure/docker/docker-compose.production.yml +++ b/infrastructure/docker/docker-compose.production.yml @@ -95,6 +95,20 @@ services: cpus: "0.5" memory: 640M + # app-tools: on-demand ops-container för db-migreringar + seed. Startar INTE med + # `up` (profile: tools). Körs via: + # docker compose -f --profile tools run --rm app-tools tsx packages/database/src/migrate.ts + # docker compose -f --profile tools run --rm app-tools tsx packages/database/src/seed/run.ts + app-tools: + build: + context: ../.. + dockerfile: infrastructure/docker/Dockerfile.tools + image: app-tools:latest + container_name: app-tools + env_file: /opt/app-platform/secrets/api.env + profiles: ["tools"] + restart: "no" + # PostgreSQL: befintlig RDS-instans med separat separat databas och # användaren app-användaren med minsta möjliga privilegier (spec §52). # Se infrastructure/deployment/create-database.sql.