diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..55914934e7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.github +.next +node_modules +docs +mcp-server +supabase +*.md +!README.md +.env* +!.env.local.example +Dockerfile +docker-compose*.yml +.dockerignore +.vscode +.idea +coverage diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..5edddc3790 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# syntax=docker/dockerfile:1 + +# --------------------------------------------------------------- +# Stage 1 — install dependencies (cached until package*.json change) +# --------------------------------------------------------------- +FROM node:22-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +# --------------------------------------------------------------- +# Stage 2 — build +# +# NEXT_PUBLIC_* values are inlined into the client bundle at build +# time, so they must be provided as build args (docker-compose.yml +# forwards them from .env.local). Server-only secrets (service role +# key, ENCRYPTION_KEY, META_APP_SECRET, ...) are read at runtime and +# must NOT be baked into the image. +# --------------------------------------------------------------- +FROM node:22-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ARG NEXT_PUBLIC_SUPABASE_URL +ARG NEXT_PUBLIC_SUPABASE_ANON_KEY +ARG NEXT_PUBLIC_SITE_URL +ARG NEXT_PUBLIC_APP_LOCALE=en +ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL \ + NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY \ + NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL \ + NEXT_PUBLIC_APP_LOCALE=$NEXT_PUBLIC_APP_LOCALE \ + NEXT_TELEMETRY_DISABLED=1 + +RUN npm run build + +# --------------------------------------------------------------- +# Stage 3 — minimal runtime (standalone output) +# --------------------------------------------------------------- +FROM node:22-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 + +RUN addgroup -S nextjs && adduser -S nextjs -G nextjs + +COPY --from=builder --chown=nextjs:nextjs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nextjs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nextjs /app/public ./public + +USER nextjs +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/README.md b/README.md index 9d9bf92d96..f20c8343f2 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,9 @@ npm run dev Open . You'll be redirected to `/login` (or `/dashboard` if already signed in). +Prefer containers? See [docs/docker.md](./docs/docker.md) for the +Dockerfile + Docker Compose setup. + ## 🚀 Deploy on Hostinger (recommended)

diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..1d1fec34b1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +name: wacrm + +services: + app: + build: + context: . + # NEXT_PUBLIC_* vars are inlined at build time — forward them + # from .env.local as build args. Changing any of them requires + # a rebuild (docker compose up --build). + args: + NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY} + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-} + NEXT_PUBLIC_APP_LOCALE: ${NEXT_PUBLIC_APP_LOCALE:-en} + # Runtime (server-only) secrets come from .env.local; they are + # never baked into the image. + env_file: + - .env.local + ports: + - "${PORT:-3000}:3000" + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://localhost:3000').then((r)=>process.exit(r.ok||r.status<500?0:1)).catch(()=>process.exit(1))", + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000000..a0748691ef --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,57 @@ +# Running with Docker + +The repo ships a multi-stage `Dockerfile` (Next.js standalone output, +runs as a non-root user) and a `docker-compose.yml` with a single +`app` service. Supabase is external — point the app at your hosted +(or self-hosted) Supabase project via env vars; no database container +is included. + +## Quick start + +1. Copy the env template and fill it in: + + ```bash + cp .env.local.example .env.local + ``` + +2. Build and start (the `--env-file` flag is required — Compose only + reads `.env` by default for `${VAR}` substitution, and this project + keeps its config in `.env.local`): + + ```bash + docker compose --env-file .env.local up --build -d + ``` + +3. The app is served on [http://localhost:3000](http://localhost:3000) + (override the host port with `PORT=8080` in `.env.local`). + +## Build-time vs runtime variables + +- `NEXT_PUBLIC_*` variables are **inlined into the client bundle at + build time**. They are passed as Docker build args by + `docker-compose.yml`. If you change any of them, rebuild: + `docker compose --env-file .env.local up --build -d`. +- Everything else (`SUPABASE_SERVICE_ROLE_KEY`, `ENCRYPTION_KEY`, + `META_APP_SECRET`, …) is read at **runtime** from `.env.local` via + `env_file` and is never baked into the image — safe to change with + just a container restart. + +## Plain Docker (no Compose) + +```bash +docker build \ + --build-arg NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co \ + --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key \ + -t wacrm . + +docker run -d --env-file .env.local -p 3000:3000 wacrm +``` + +## Notes + +- Database migrations under `supabase/` are **not** run by the + container — apply them with the Supabase CLI as described in the + README. +- If you use automation Wait steps, remember to point your cron pinger + at `GET /api/automations/cron` on this deployment (see + `docs/automations-and-cron.md`). diff --git a/next.config.ts b/next.config.ts index 7ac8a173ca..f12bd50730 100644 --- a/next.config.ts +++ b/next.config.ts @@ -64,6 +64,11 @@ const SECURITY_HEADERS = [ ] as const; const nextConfig: NextConfig = { + // Emit a self-contained server bundle (.next/standalone) so the + // Docker image can run without node_modules or the Next CLI. + // Harmless outside Docker: `next start` keeps working as before. + output: "standalone", + /** * Cache-Control policy. *