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_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_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
FROM node:24-alpine AS build
|
||||
RUN corepack enable
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm build
|
||||
|
||||
# --- Runtime stage: minimal image running the built server ---
|
||||
|
||||
@@ -3,7 +3,7 @@ RUN corepack enable
|
||||
WORKDIR /app
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
## Status
|
||||
@@ -33,14 +33,35 @@ House of Quality: **F1 board definition → F2/F3 filing → F4/F5 weak-point ma
|
||||
|
||||
## 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) ·
|
||||
Docker Compose on Coolify.
|
||||
Docker on Coolify.
|
||||
|
||||
## Deployment notes
|
||||
## Running with Docker
|
||||
|
||||
- **Use Coolify's managed Postgres** (with scheduled backups); the app connects
|
||||
via `DATABASE_URL`. See [ADR 0003](./docs/adr/0003-use-coolify-managed-postgres-over-sqlite.md).
|
||||
- **Don't auto-deploy from `main`** — every merge would deploy. Use a tagged
|
||||
release or manual trigger.
|
||||
- **Don't trust the OAuth `hd` claim alone** — verify the email domain server-side.
|
||||
```bash
|
||||
# Production-like full stack (build the image + Postgres)
|
||||
docker compose up --build # app on http://localhost:3000
|
||||
|
||||
# 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:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgres://andon:andon@db:5432/andon
|
||||
HOST: 0.0.0.0
|
||||
DATABASE_URL: ${DATABASE_URL:-postgres://andon:andon@db:5432/andon}
|
||||
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:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: andon
|
||||
POSTGRES_PASSWORD: andon
|
||||
POSTGRES_DB: andon
|
||||
ports:
|
||||
- "5432:5432"
|
||||
POSTGRES_USER: ${POSTGRES_USER:-andon}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-andon}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-andon}
|
||||
volumes:
|
||||
- andon_pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U andon"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-andon}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
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.
|
||||
- **Supersedes** the SQLite choice in DESIGN.md (T5, F10) and the original
|
||||
`/deep-design` decision.
|
||||
- Unchanged: deploys must **not** auto-run from `main` (every merge would deploy);
|
||||
use a tagged release or manual trigger.
|
||||
- Autodeploy from `main` is enabled (Coolify watches the repo and deploys on
|
||||
push); `main` must be protected with required PR review + CI so only vetted
|
||||
commits reach production.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "andon",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.0.9",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
@@ -14,7 +15,10 @@
|
||||
"typecheck": "nuxt typecheck",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"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": {
|
||||
"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
|
||||
scheduled backups; configure the two domains and TLS on Coolify; wire
|
||||
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:**
|
||||
- [ ] 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 |
|
||||
|------|--------|------------|
|
||||
| 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 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) |
|
||||
|
||||
Reference in New Issue
Block a user