ci: Coolify autodeploy from main; migrate-on-boot and split dev/prod compose
- Add Nitro plugin to apply Drizzle migrations on server boot (skips when DATABASE_URL is unset, e.g. during build). - Split compose: docker-compose.dev.yml (hot-reload + Postgres) vs docker-compose.yml (production/self-host, env-driven, Coolify-deployable). - Add .dockerignore; parameterise compose env; document the autodeploy decision (Coolify watches main, no CI workflow needed) in ADR 0003 and plan.md T12.
This commit is contained in:
17
.dockerignore
Normal file
17
.dockerignore
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.git
|
||||||
|
node_modules
|
||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
docker-compose*.yml
|
||||||
|
Dockerfile*
|
||||||
|
tests
|
||||||
|
docs
|
||||||
|
tasks
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
@@ -4,6 +4,11 @@ NUXT_SESSION_SECRET=change-me
|
|||||||
# Database (Postgres) — Coolify-managed in prod, docker-compose in dev
|
# Database (Postgres) — Coolify-managed in prod, docker-compose in dev
|
||||||
DATABASE_URL=postgres://andon:andon@localhost:5432/andon
|
DATABASE_URL=postgres://andon:andon@localhost:5432/andon
|
||||||
|
|
||||||
|
# Credentials for the bundled Postgres service in docker-compose.yml
|
||||||
|
POSTGRES_USER=andon
|
||||||
|
POSTGRES_PASSWORD=andon
|
||||||
|
POSTGRES_DB=andon
|
||||||
|
|
||||||
# Google OAuth (Task 9) — restricted to the theodo.com hosted domain
|
# Google OAuth (Task 9) — restricted to the theodo.com hosted domain
|
||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
|||||||
@@ -4,9 +4,8 @@
|
|||||||
FROM node:24-alpine AS build
|
FROM node:24-alpine AS build
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json pnpm-lock.yaml ./
|
|
||||||
RUN pnpm install --frozen-lockfile
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
# --- Runtime stage: minimal image running the built server ---
|
# --- Runtime stage: minimal image running the built server ---
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ RUN corepack enable
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install dependencies first for better layer caching.
|
# Install dependencies first for better layer caching.
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -10,7 +10,7 @@ Two apps, one codebase, behind Google SSO (`@theodo.com`):
|
|||||||
|
|
||||||
- **`andon.apoena.dev`** — the reporting view: click an ASCII board section, pick
|
- **`andon.apoena.dev`** — the reporting view: click an ASCII board section, pick
|
||||||
your project, describe the problem.
|
your project, describe the problem.
|
||||||
- **`dashboard.andon.apoena.dev`** — the same board as a red-dot defect map, a
|
- **`dashboard-andon.apoena.dev`** — the same board as a red-dot defect map, a
|
||||||
reverse-chronological feed, and verbatims in a modal.
|
reverse-chronological feed, and verbatims in a modal.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
@@ -33,14 +33,35 @@ House of Quality: **F1 board definition → F2/F3 filing → F4/F5 weak-point ma
|
|||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
Nuxt 3 (full-stack Vue) · PostgreSQL + Drizzle (Coolify-managed, auto-backups) ·
|
Nuxt 4 (full-stack Vue) · PostgreSQL + Drizzle (Coolify-managed, auto-backups) ·
|
||||||
Google OAuth (`hd=theodo.com` + server-side domain recheck) · Web Push (PWA) ·
|
Google OAuth (`hd=theodo.com` + server-side domain recheck) · Web Push (PWA) ·
|
||||||
Docker Compose on Coolify.
|
Docker on Coolify.
|
||||||
|
|
||||||
## Deployment notes
|
## Running with Docker
|
||||||
|
|
||||||
- **Use Coolify's managed Postgres** (with scheduled backups); the app connects
|
```bash
|
||||||
via `DATABASE_URL`. See [ADR 0003](./docs/adr/0003-use-coolify-managed-postgres-over-sqlite.md).
|
# Production-like full stack (build the image + Postgres)
|
||||||
- **Don't auto-deploy from `main`** — every merge would deploy. Use a tagged
|
docker compose up --build # app on http://localhost:3000
|
||||||
release or manual trigger.
|
|
||||||
- **Don't trust the OAuth `hd` claim alone** — verify the email domain server-side.
|
# Local development (hot-reload, source-mounted)
|
||||||
|
docker compose -f docker-compose.dev.yml up
|
||||||
|
|
||||||
|
# Or the dev server directly against a containerized Postgres
|
||||||
|
docker compose -f docker-compose.dev.yml up -d db
|
||||||
|
DATABASE_URL=postgres://andon:andon@localhost:5432/andon pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Migrations are applied automatically on server boot (a Nitro plugin) — no manual
|
||||||
|
migrate step is needed.
|
||||||
|
|
||||||
|
## Deploying to Coolify
|
||||||
|
|
||||||
|
1. Point Coolify at this repo; it builds the production `Dockerfile`.
|
||||||
|
2. Create a **managed Postgres** in Coolify (automatic backups — [ADR 0003](./docs/adr/0003-use-coolify-managed-postgres-over-sqlite.md))
|
||||||
|
and set `DATABASE_URL` to it. _(Alternatively deploy `docker-compose.yml`, which bundles a Postgres service.)_
|
||||||
|
3. Set the env vars from [`.env.example`](./.env.example) (`NUXT_SESSION_SECRET`,
|
||||||
|
Google OAuth, VAPID) and route `andon.apoena.dev` + `dashboard-andon.apoena.dev` to the service.
|
||||||
|
4. **Enable Coolify's autodeploy** so it builds and deploys on every push to
|
||||||
|
`main`. Protect `main` with required PR review + CI so only vetted commits
|
||||||
|
reach production.
|
||||||
|
5. The OAuth `hd` claim is spoofable — the app re-checks the email domain server-side (T9).
|
||||||
|
|||||||
37
docker-compose.dev.yml
Normal file
37
docker-compose.dev.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Local development stack: app with hot-reload (source bind-mounted) + Postgres.
|
||||||
|
# docker compose -f docker-compose.dev.yml up
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgres://andon:andon@db:5432/andon
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: andon
|
||||||
|
POSTGRES_PASSWORD: andon
|
||||||
|
POSTGRES_DB: andon
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- andon_pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U andon"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
andon_pgdata:
|
||||||
@@ -1,32 +1,40 @@
|
|||||||
|
# Production / self-host stack (Coolify-deployable as a Docker Compose resource).
|
||||||
|
# For production, prefer a Coolify-managed Postgres (automatic backups — see
|
||||||
|
# docs/adr/0003) by setting DATABASE_URL to it; the bundled `db` service is for
|
||||||
|
# self-hosting and local production testing.
|
||||||
|
# docker compose up --build
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.dev
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgres://andon:andon@db:5432/andon
|
DATABASE_URL: ${DATABASE_URL:-postgres://andon:andon@db:5432/andon}
|
||||||
HOST: 0.0.0.0
|
NUXT_SESSION_SECRET: ${NUXT_SESSION_SECRET:-change-me}
|
||||||
|
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
||||||
|
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
||||||
|
GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-}
|
||||||
|
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-}
|
||||||
|
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-}
|
||||||
|
VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:owner@theodo.com}
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
volumes:
|
|
||||||
- .:/app
|
|
||||||
- /app/node_modules
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: andon
|
POSTGRES_USER: ${POSTGRES_USER:-andon}
|
||||||
POSTGRES_PASSWORD: andon
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-andon}
|
||||||
POSTGRES_DB: andon
|
POSTGRES_DB: ${POSTGRES_DB:-andon}
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
volumes:
|
volumes:
|
||||||
- andon_pgdata:/var/lib/postgresql/data
|
- andon_pgdata:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U andon"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-andon}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|||||||
@@ -18,5 +18,6 @@ switch to **Coolify-managed PostgreSQL** (via Drizzle's `pg` driver).
|
|||||||
prod — one more moving part than a single file.
|
prod — one more moving part than a single file.
|
||||||
- **Supersedes** the SQLite choice in DESIGN.md (T5, F10) and the original
|
- **Supersedes** the SQLite choice in DESIGN.md (T5, F10) and the original
|
||||||
`/deep-design` decision.
|
`/deep-design` decision.
|
||||||
- Unchanged: deploys must **not** auto-run from `main` (every merge would deploy);
|
- Autodeploy from `main` is enabled (Coolify watches the repo and deploys on
|
||||||
use a tagged release or manual trigger.
|
push); `main` must be protected with required PR review + CI so only vetted
|
||||||
|
commits reach production.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "andon",
|
"name": "andon",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "pnpm@11.0.9",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
@@ -14,7 +15,10 @@
|
|||||||
"typecheck": "nuxt typecheck",
|
"typecheck": "nuxt typecheck",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "tsx server/db/migrate.ts",
|
"db:migrate": "tsx server/db/migrate.ts",
|
||||||
"db:verify": "tsx scripts/verify-db.ts"
|
"db:verify": "tsx scripts/verify-db.ts",
|
||||||
|
"docker:dev": "docker compose -f docker-compose.dev.yml up",
|
||||||
|
"docker:dev:build": "docker compose -f docker-compose.dev.yml up --build",
|
||||||
|
"docker:dev:down": "docker compose -f docker-compose.dev.yml down"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
|
|||||||
13
server/plugins/migrate.ts
Normal file
13
server/plugins/migrate.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { migrate } from 'drizzle-orm/node-postgres/migrator'
|
||||||
|
import { getDb } from '../db/client'
|
||||||
|
|
||||||
|
// Apply pending migrations once when the server boots (dev and prod). Skipped
|
||||||
|
// when DATABASE_URL is unset (e.g. during build) so it never blocks the bundle.
|
||||||
|
export default defineNitroPlugin(async () => {
|
||||||
|
if (!process.env.DATABASE_URL) {
|
||||||
|
console.warn('[migrate] DATABASE_URL not set — skipping migrations')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await migrate(getDb(), { migrationsFolder: 'server/db/migrations' })
|
||||||
|
console.log('[migrate] migrations applied')
|
||||||
|
})
|
||||||
@@ -313,7 +313,8 @@ section, project, and a verbatim snippet; log failures (no retry queue in v1).
|
|||||||
**Description:** Production Dockerfile; provision Coolify-managed Postgres with
|
**Description:** Production Dockerfile; provision Coolify-managed Postgres with
|
||||||
scheduled backups; configure the two domains and TLS on Coolify; wire
|
scheduled backups; configure the two domains and TLS on Coolify; wire
|
||||||
env/secrets (`DATABASE_URL`, Google client, VAPID, session secret); run the
|
env/secrets (`DATABASE_URL`, Google client, VAPID, session secret); run the
|
||||||
migration on boot. Deploy from a tagged release/manual trigger, not auto-`main`.
|
migration on boot. Coolify autodeploys on push to `main`, with `main`
|
||||||
|
protected by PR review + CI.
|
||||||
|
|
||||||
**Acceptance criteria:**
|
**Acceptance criteria:**
|
||||||
- [ ] After a redeploy, previously filed defects are still present (0 data loss).
|
- [ ] After a redeploy, previously filed defects are still present (0 data loss).
|
||||||
@@ -339,7 +340,7 @@ migration on boot. Deploy from a tagged release/manual trigger, not auto-`main`.
|
|||||||
| Risk | Impact | Mitigation |
|
| Risk | Impact | Mitigation |
|
||||||
|------|--------|------------|
|
|------|--------|------------|
|
||||||
| Data lost on redeploy / disk failure | High | Coolify-managed Postgres with scheduled backups; redeploy durability test (T12, ADR 0003) |
|
| Data lost on redeploy / disk failure | High | Coolify-managed Postgres with scheduled backups; redeploy durability test (T12, ADR 0003) |
|
||||||
| Auto-deploy from `main` ships every merge | Med | Deploy from tagged release / manual trigger only (T12) |
|
| Autodeploy from `main` ships every merge | Med | Protect `main` with required PR review + CI; migrations-on-boot + healthcheck catch failures; Coolify keeps rollback (T12) |
|
||||||
| Google `hd` claim spoofed by a custom client | High | Server-side verified-email-domain recheck (T9) |
|
| Google `hd` claim spoofed by a custom client | High | Server-side verified-email-domain recheck (T9) |
|
||||||
| Google OAuth credentials/redirect URIs unavailable | Med | `getReporter()` seam lets Phases 1–3 proceed with dev auth (T5, T9) |
|
| Google OAuth credentials/redirect URIs unavailable | Med | `getReporter()` seam lets Phases 1–3 proceed with dev auth (T5, T9) |
|
||||||
| Dot map unreadable on hot Sections | Med | Visible-dot cap + "+N more" (T7) |
|
| Dot map unreadable on hot Sections | Med | Visible-dot cap + "+N more" (T7) |
|
||||||
|
|||||||
Reference in New Issue
Block a user