diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c2a2987 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/node_modules +**/.next +**/.turbo +**/dist +**/build +**/.git +**/.gitignore +**/.env +**/.env.local +**/.env.*.local +**/npm-debug.log +**/yarn-debug.log +**/yarn-error.log +**/*.log +**/cypress/videos +**/cypress/screenshots +**/.DS_Store +**/Thumbs.db +**/.idea +**/.vscode +**/coverage +**/.cache +**/.eslintcache +**/test-results +**/playwright-report +Dockerfile +.dockerignore +README.md +docs +*.pdf diff --git a/DEPLOY_COOLIFY.md b/DEPLOY_COOLIFY.md new file mode 100644 index 0000000..97b1463 --- /dev/null +++ b/DEPLOY_COOLIFY.md @@ -0,0 +1,102 @@ +# Garage dashboard (garage-erp) — Coolify deploy + +Domain: **tenant.reparee.com** +Build: Docker (multi-stage), Next.js standalone output +Port: **3000** inside container; Coolify maps to 80/443 + TLS + +--- + +## 1. New Application in Coolify + +1. **+ New Resource → Application → Public Repository** (or GitHub App). +2. Repo: this monorepo. Branch: `main` (or whatever). +3. **Build Pack: Dockerfile**. +4. **Base Directory**: `garage-erp` (where the Dockerfile lives — relative to repo root). + - If `garage-erp` IS the repo root, leave blank. +5. **Dockerfile Location**: `Dockerfile` (default). +6. **Port**: `3000`. + +## 2. Environment variables + +Coolify → Application → **Environment Variables**. Add: + +| Key | Value | +|---|---| +| `NEXT_PUBLIC_API_URL` | `https://api.reparee.com` | +| `NEXT_PUBLIC_SAAS_URL` | `https://reparee.com` | +| `NEXT_PUBLIC_GARAGE_HOST_PATTERN` | `*.reparee.com` | +| `NODE_ENV` | `production` | +| `HOSTNAME` | `0.0.0.0` | +| `PORT` | `3000` | +| `NEXT_TELEMETRY_DISABLED` | `1` | + +> `NEXT_PUBLIC_*` values are **baked into the JS bundle at build time**. Changing them requires a redeploy, not just a restart. + +## 3. Domain + TLS + +Coolify → Application → **Domains** → add `https://tenant.reparee.com`. Coolify auto-provisions Let's Encrypt cert. DNS A record must already point to the Coolify server's public IP. + +## 4. Healthcheck + +Coolify → Application → **Healthcheck**: + +- **Path**: `/api/proxy/health` — set this once the backend is reachable; otherwise `/` works for the first deploy. +- **Port**: `3000` +- **Interval**: 30s +- **Timeout**: 10s + +## 5. Build options + +- **Build pack**: Dockerfile +- **Custom build command**: leave blank (Dockerfile handles it) +- **Build args**: none required; all envs are runtime + +## 6. Deploy + +Click **Deploy**. Watch build logs: + +- Stage 1 (`deps`): `pnpm install --frozen-lockfile` — ~2-4 min. +- Stage 3 (`builder`): `pnpm --filter @garage/dashboard build` — ~3-6 min. Watch for Next.js trace warnings; they are non-fatal. +- Stage 4 (`runner`): copies standalone output; tiny. + +First build is slow. Subsequent builds reuse Docker layer cache. + +## 7. Smoke test + +```bash +curl -I https://tenant.reparee.com +# → 200 OK + Set-Cookie not yet set (no session) + +# Activation: full flow goes SaaS → /activate/handoff/{token}?ws=&api= +# Cookies expected after redirect: auth_token, auth_user, workspace_uuid, api_base_url +``` + +## 8. CORS / cookies sanity + +The dashboard only sets cookies on its own origin (`tenant.reparee.com`). The Next.js server proxy at `/api/proxy/*` strips upstream `Set-Cookie` headers — Laravel cookies never reach the browser. Workspace context is read from cookies on every server-side proxy call. + +Verify in browser DevTools after handoff: + +- 4 cookies on `tenant.reparee.com`, all HttpOnly + SameSite=Strict + Secure. +- Network tab: all `XHR/fetch` go to `tenant.reparee.com/api/proxy/...`. **Zero direct calls** to `api.reparee.com` from the browser. + +## 9. Logs + +Coolify → Application → **Logs** tab streams container stdout/stderr. Next.js logs requests as `GET / 200 in 45ms` etc. + +## 10. Rollback + +Coolify → **Deployments** tab → pick a previous successful deployment → **Redeploy**. + +## 11. Notes on monorepo + +- The Dockerfile builds **only** the `@garage/dashboard` app. `@garage/api` is built as part of dashboard's `prebuild` hook (it generates OpenAPI types from `postman/collection.json`). +- If you split apps later (e.g. a separate admin app), copy the Dockerfile and change the `--filter @garage/dashboard` line. +- `output: 'standalone'` in `apps/dashboard/next.config.mjs` is required for the runner stage to work. Do not remove. + +## 12. Cypress / e2e + +Out of scope for the runtime container. If you want CI e2e runs: + +- Use Coolify's **GitHub Actions integration** to run `pnpm test:e2e` in CI before the deploy webhook fires. +- Update `apps/dashboard/cypress.config.ts` to read `baseUrl` from env (currently hard-coded to `https://newgarage.yslootahtech.com`). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a9a2759 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,65 @@ +# Multi-stage Dockerfile for garage-erp dashboard (Coolify). +# Monorepo: turborepo + pnpm; @garage/dashboard depends on @garage/api. +# Build context = garage-erp/ (repo root for this app). + +# ──────────────────────────────────────────────────────────── +# Stage 1: base — pnpm via corepack, shared layer +# ──────────────────────────────────────────────────────────── +FROM node:22-alpine AS base +RUN corepack enable && corepack prepare pnpm@9.0.0 --activate +WORKDIR /app + +# ──────────────────────────────────────────────────────────── +# Stage 2: deps — install all workspace deps +# ──────────────────────────────────────────────────────────── +FROM base AS deps +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./ +COPY apps/dashboard/package.json ./apps/dashboard/ +COPY packages/api/package.json ./packages/api/ +COPY packages/ui/package.json ./packages/ui/ +COPY packages/eslint-config/package.json ./packages/eslint-config/ +COPY packages/typescript-config/package.json ./packages/typescript-config/ +RUN pnpm install --frozen-lockfile + +# ──────────────────────────────────────────────────────────── +# Stage 3: builder — build dashboard with standalone output +# ──────────────────────────────────────────────────────────── +FROM base AS builder +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/apps/dashboard/node_modules ./apps/dashboard/node_modules +COPY --from=deps /app/packages/api/node_modules ./packages/api/node_modules +COPY . . + +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production + +# Build only the dashboard (turbo handles @garage/api codegen via prebuild hook) +RUN pnpm --filter @garage/dashboard build + +# ──────────────────────────────────────────────────────────── +# Stage 4: runner — minimal runtime, standalone Next.js +# ──────────────────────────────────────────────────────────── +FROM node:22-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Standalone server + traced node_modules +COPY --from=builder --chown=nextjs:nodejs /app/apps/dashboard/.next/standalone ./ +# Static assets +COPY --from=builder --chown=nextjs:nodejs /app/apps/dashboard/.next/static ./apps/dashboard/.next/static +# Public assets (logos, etc.) +COPY --from=builder --chown=nextjs:nodejs /app/apps/dashboard/public ./apps/dashboard/public + +USER nextjs + +EXPOSE 3000 + +# Standalone entry is at apps/dashboard/server.js after copy +CMD ["node", "apps/dashboard/server.js"] diff --git a/apps/dashboard/.env.production.example b/apps/dashboard/.env.production.example new file mode 100644 index 0000000..82c102c --- /dev/null +++ b/apps/dashboard/.env.production.example @@ -0,0 +1,16 @@ +# Production env — Coolify provides these at deploy time. + +# Fallback backend URL — only used if the per-session `api_base_url` cookie +# is not set. SaaS handoff sets that cookie; server proxy uses it. +NEXT_PUBLIC_API_URL=https://api.reparee.com + +# Used by "Workspace session not found" page CTA back to SaaS. +NEXT_PUBLIC_SAAS_URL=https://reparee.com + +# Image remotePatterns wildcard for SaaS-provisioned subdomains. +NEXT_PUBLIC_GARAGE_HOST_PATTERN=*.reparee.com + +# Forces Next to bind 0.0.0.0 in Docker. Coolify normally sets this. +HOSTNAME=0.0.0.0 +PORT=3000 +NODE_ENV=production diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs index 7956448..a6ff1c6 100644 --- a/apps/dashboard/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -1,5 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + output: 'standalone', images: { remotePatterns: [ @@ -21,6 +22,24 @@ const nextConfig = { port: '', pathname: '/**', }, + { + protocol: 'https', + hostname: 'api.reparee.com', + port: '', + pathname: '/**', + }, + { + protocol: 'https', + hostname: 'reparee.com', + port: '', + pathname: '/**', + }, + { + protocol: 'https', + hostname: 'tenant.reparee.com', + port: '', + pathname: '/**', + }, { protocol: 'https', hostname: process.env.NEXT_PUBLIC_GARAGE_HOST_PATTERN ?? '*.reparee.com',