feat(ops): saker deploy-process - tools-container for migrera/seed + omskriven deploy.sh + runbook
CI / Typecheck, test & build (push) Failing after 49s
CI / Typecheck, test & build (push) Failing after 49s
- Dockerfile.tools + app-tools compose-tjanst (profile: tools): on-demand migrera/seed via tsx, kor aldrig som tjanst. - deploy.sh omskriven: ratt gren (master), ratt sokvag (/opt/cibello-platform), riktig migrering via tools-container (ersatter trasig --migrate-only), bygger api+worker, healthcheck med automatisk rollback. - RUNBOOK.md: deploy, recept-/sprak-uppdatering, migreringar, rollback, admin. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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: <sha> (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 <föregående-sha>
|
||||
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
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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 <compose> --profile tools run --rm app-tools tsx packages/database/src/migrate.ts
|
||||
# docker compose -f <compose> --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')"]
|
||||
@@ -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 <compose> --profile tools run --rm app-tools tsx packages/database/src/migrate.ts
|
||||
# docker compose -f <compose> --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.
|
||||
|
||||
Reference in New Issue
Block a user