From c08015ed9f9ef233a2ecd76e94735aba04040324 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Wed, 12 Aug 2026 23:10:49 -0400 Subject: [PATCH] chore(deploy): add Coolify-ready Dockerfile and compose stack --- .dockerignore | 69 +++++++++++++++++++ .env.example | 64 ++++++++++++------ Dockerfile | 160 ++++++++++++++++++++++++++++++--------------- docker-compose.yml | 91 ++++++++++++++++---------- 4 files changed, 280 insertions(+), 104 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6821b9a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,69 @@ +# --- Version control / CI --- +# NOTE: `.git` is intentionally INCLUDED in the build context — the builder +# stage runs `apk add git` so `generate:version-info` can read commit +# metadata (commitHash, gitDescribe, commitDate) into versionInfo.json, +# which ships in the image and is shown by the admin VersionOverlay. +# Keep .git out only if you don't care about that overlay being populated. +.github +.junie + +# --- Dependencies (reinstalled in the image) --- +node_modules +**/node_modules + +# --- Next.js build artifacts (regenerated by build stage) --- +.next +out +build + +# --- Local runtime data --- +media +mission-files + +# --- Tests + Playwright artifacts (not needed for the production image) --- +tests +test-results +playwright-report +blob-report +playwright-report +.playwright-mcp +test.env +playwright.config.ts +vitest.config.mts +vitest.setup.ts + +# --- Editör / IDE / local tooling --- +.idea +.vscode +.opencode +.omo +.codegraph +.logs + +# --- Generated / build-info --- +tsconfig.tsbuildinfo +next-env.d.ts + +# --- Env files (injected by Coolify per environment) --- +.env +.env.* +!.env.example + +# --- Misc project noise --- +README.md +TODO.md +AGENTS.md +DEPLOYMENT.md +docs +Writerside +repro-account.ts +reset-test-user-password.ts +pnpm-lock.yaml.bk +.yarnrc +.npmrc + +# Docker own files (avoid recursion) +Dockerfile +.dockerignore +docker-compose.yml +docker-compose.*.yml \ No newline at end of file diff --git a/.env.example b/.env.example index 8ca50b4..b4fd7d4 100644 --- a/.env.example +++ b/.env.example @@ -1,24 +1,50 @@ -DATABASE_URI=mongodb://127.0.0.1/your-database-name +# --- Database (PostgreSQL) --- +# Coolify: leave this BLANK in the application's env here — Coolify injects +# DATABASE_URI automatically from the per-environment PostgreSQL service +# (Project → New Resource → PostgreSQL). Use the format Coolify emits. +# Local docker compose: override via POSTGRES_USER/_PASSWORD/_DB in your +# local `.env` (the compose file rewrites DATABASE_URI from those). +DATABASE_URI=postgres://ptfapp:polaristaskforcedb@127.0.0.1:5432/ptf-app-dev + +# Random 32+ char secret used to sign Payload sessions / JWTs. +# Generate with: `openssl rand -hex 16` PAYLOAD_SECRET=YOUR_SECRET_HERE -# Public URL of the running app (used by the game tick script to notify clients) +# --- Public URL of the running app --- +# Used by the game-tick script (POST /api/game-tick/notify) and by Payload +# for absolute admin URLs. In Coolify set this to your environment's +# public domain (e.g. https://dev.ptf.example.com). APP_URL=http://localhost:3000 -# Secret that guards the /api/game-tick/notify endpoint. The game tick script -# sends this header so connected clients refresh after each tick. + +# --- Game tick notify secret --- +# Shared secret that guards the /api/game-tick/notify endpoint. Generate +# the same value here and pass it to the scheduled `docker exec` job on +# Coolify (or to your cron). See DEPLOYMENT.md. GAME_TICK_NOTIFY_SECRET=YOUR_GAME_TICK_NOTIFY_SECRET -# --- Discord bot --- -# Bot token from the Discord Developer Portal. Required to run the bot. -DISCORD_TOKEN= -# Server (guild) id that slash commands are registered to. Required. -DISCORD_GUILD_ID= -# Channel id where attendance embeds are posted. -DISCORD_OPS_CHANNEL_ID= -# Channel id used as the default /announce target. -DISCORD_ANNOUNCE_CHANNEL_ID= -# Comma-separated Discord role ids that grant staff permissions. -DISCORD_STAFF_ROLE_IDS= -# How often the attendance reconcile loop runs, in ms. Defaults to 60000. -DISCORD_ATTENDANCE_POLL_MS= -# How often the notification bridge polls for new notifications, in ms. Defaults to 20000. -DISCORD_NOTIFICATION_POLL_MS= +# --- Payload email (nodemailer / SMTP relay) --- +# Optional. Leave blank to disable outbound email (password resets, etc). +EMAIL_FROM_ADDRESS= +EMAIL_FROM_NAME= +EMAIL_HOST= +EMAIL_PORT=587 +EMAIL_USERNAME= +EMAIL_PASSWORD= + +# --- Docker compose tunables (local dev only — not used by Coolify) --- +POSTGRES_USER=ptfapp +POSTGRES_PASSWORD=polaristaskforcedb +POSTGRES_DB=ptf-app-dev +POSTGRES_PORT=5432 +APP_PORT=3000 + +# --- Discord bot (disabled / not deployed yet) --- +# Kept here for documentation. The bot is excluded from the deployment image +# per DEPLOYMENT.md — these only apply when running the bot standalone. +# DISCORD_TOKEN= +# DISCORD_GUILD_ID= +# DISCORD_OPS_CHANNEL_ID= +# DISCORD_ANNOUNCE_CHANNEL_ID= +# DISCORD_STAFF_ROLE_IDS= +# DISCORD_ATTENDANCE_POLL_MS= +# DISCORD_NOTIFICATION_POLL_MS= \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 20be634..c963d63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,71 +1,129 @@ -# To use this Dockerfile, you have to set `output: 'standalone'` in your next.config.mjs file. -# From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile +# syntax=docker/dockerfile:1.7 +# +# Polaris Task Force — production image for Coolify. +# +# Stack: Next.js 16 (output: "standalone") + Payload CMS 3.79 + Bun 1.3.11. +# Installed and built with Bun; the Next.js runtime runs on Node 22 +# (Next's standalone server is a node script), and Bun is kept in the +# runner image so the Payload CLI bin scripts (game-tick / market-tick / +# migrate) work via `docker exec ... bun run payload ` against the +# running container. +# +# See DEPLOYMENT.md for the full Coolify workflow (per-env Postgres, +# env vars, scheduled jobs, persistent media volume, rollback). -FROM node:22.17.0-alpine AS base +# ───────────────────────────── base ───────────────────────────── +# Node 22 alpine + Bun, shared by all three stages. +FROM node:22-alpine AS base +RUN apk add --no-cache libc6-compat wget \ + && npm install -g bun@1.3.11 \ + && bun --version && node --version -# Install dependencies only when needed -FROM base AS deps -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat +# ────────────────────────── prod-deps ──────────────────────────── +# Production-only install. Used at runtime alongside the standalone +# server so the Payload CLI / migrations / bin scripts have every +# package they need (the Next standalone prunes node_modules to only +# what the web server imports — pruned tree does NOT include `payload`, +# `@payloadcms/*` admin libs, drizzle, etc.). +FROM base AS prod-deps WORKDIR /app +COPY package.json bun.lock ./ +RUN bun install --production --frozen-lockfile -# Install dependencies based on the preferred package manager -COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ -RUN \ - if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ - elif [ -f package-lock.json ]; then npm ci; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ - else echo "Lockfile not found." && exit 1; \ - fi - - -# Rebuild the source code only when needed +# ─────────────────────────── builder ───────────────────────────── +# Full install (incl. devDeps) + Next.js standalone build. FROM base AS builder +# `git` is needed only at build time so `generate:version-info` can populate +# commitHash / gitDescribe / commitDate in src/generated/versionInfo.json +# (the script gracefully no-ops without it, but we want the metadata in the +# admin VersionOverlay). Not carried into the runner image. +RUN apk add --no-cache git WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile COPY . . +# next.config.mjs requires `output: "standalone"` (already set). +# `bun run build` == `bun run generate:version-info && next build --webpack`. +# We deliberately DO NOT run `payload generate:importmap` / `generate:types` +# here: both bootstrap the Payload config, which connects to the database +# (none is available at build time). The committed artifacts in the repo +# are the source of truth and ship as-is. +# +# Build-time-only env shim: Next.js prerenders /login and other pages that +# import `@payload-config` (Payload.init runs at import time during static +# generation). Payload refuses to init without PAYLOAD_SECRET, and Nodemailer +# emits a (non-fatal) warning without EMAIL_* — both come from real env vars +# at runtime, NOT baked into the image. We pass stand-in values via ARGs to +# let the build's static-gen step complete. These values never ship: ENV in +# later stages and the runtime container's env (set by Coolify per env) win. +ARG PAYLOAD_SECRET_BUILD=dummy-build-secret-not-used-at-runtime +ARG EMAIL_HOST_BUILD=localhost +ARG EMAIL_PORT_BUILD=587 +ENV PAYLOAD_SECRET=$PAYLOAD_SECRET_BUILD \ + EMAIL_HOST=$EMAIL_HOST_BUILD \ + EMAIL_PORT=$EMAIL_PORT_BUILD +RUN bun run build -# Next.js collects completely anonymous telemetry data about general usage. -# Learn more here: https://nextjs.org/telemetry -# Uncomment the following line in case you want to disable telemetry during the build. -# ENV NEXT_TELEMETRY_DISABLED 1 - -RUN \ - if [ -f yarn.lock ]; then yarn run build; \ - elif [ -f package-lock.json ]; then npm run build; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ - else echo "Lockfile not found." && exit 1; \ - fi - -# Production image, copy all the files and run next +# ─────────────────────────── runner ────────────────────────────── +# Final production image: Next standalone runtime + full prod deps +# (for payload bins) + src/migrations (for `bun run payload `). FROM base AS runner WORKDIR /app -ENV NODE_ENV production -# Uncomment the following line in case you want to disable telemetry during runtime. -# ENV NEXT_TELEMETRY_DISABLED 1 +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 \ + PNPM_HOME=/app -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +# The `node` user already exists in `node:*-alpine` images. +RUN mkdir -p /app/media \ + && chown -R node:node /app -# Remove this line if you do not have this folder -COPY --from=builder /app/public ./public +# 1) Next.js standalone runtime (server.js + traced node_modules + .next). +# `.next/standalone` is laid out flat at /app, so server.js ends up at +# /app/server.js and its traced node_modules at /app/node_modules. +COPY --from=builder --chown=node:node /app/.next/standalone ./ +# 2) Static assets (standalone does NOT include these). +COPY --from=builder --chown=node:node /app/.next/static ./.next/static +# 3) Public assets (standalone does NOT include these either). +COPY --from=builder --chown=node:node /app/public ./public -# Set the correct permission for prerender cache -RUN mkdir .next -RUN chown nextjs:nodejs .next +# 4) Overlay the FULL production node_modules on top of the pruned +# standalone one. Docker COPY merges directories file-by-file +# (existing files overwritten, others preserved), so the result +# is the union — effectively the full prod install — while keeping +# any Next-internal files the standalone tree brought along. +COPY --from=prod-deps --chown=node:node /app/node_modules ./node_modules -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +# 5) Source + top-level config so `bun run payload ` and +# `payload migrate` work via `docker exec`. These read +# @payload-config (alias in tsconfig.json) and the Payload +# migration files under src/migrations/*. +COPY --from=builder --chown=node:node /app/src ./src +COPY --from=builder --chown=node:node /app/package.json ./package.json +COPY --from=builder --chown=node:node /app/tsconfig.json ./tsconfig.json +COPY --from=builder --chown=node:node /app/bun.lock ./bun.lock +COPY --from=builder --chown=node:node /app/next.config.mjs ./next.config.mjs +COPY --from=builder --chown=node:node /app/postcss.config.mjs ./postcss.config.mjs +COPY --from=builder --chown=node:node /app/components.json ./components.json +COPY --from=builder --chown=node:node /app/drizzle.config.ts ./drizzle.config.ts -USER nextjs +# Persistent storage mount point for locally-stored Payload uploads. +# In Coolify: attach a Persistent Storage Volume here so uploaded media +# survives container restarts and rollbacks. (See DEPLOYMENT.md.) +VOLUME ["/app/media"] EXPOSE 3000 -ENV PORT 3000 +# Liveness probe hooked into our `/api/health` route (no DB dependency, +# so a transient DB outage does NOT cycle the container). +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD wget -qO- "http://127.0.0.1:${PORT:-3000}/api/health" >/dev/null 2>&1 || exit 1 -# server.js is created by next build from the standalone output -# https://nextjs.org/docs/pages/api-reference/next-config-js/output -CMD HOSTNAME="0.0.0.0" node server.js +USER node + +# Next.js standalone server (node process). Plain `node server.js` — Bun is +# installed for the `docker exec` one-shot bins, not for the web runtime +# (Next is shipped and tested on Node). +CMD ["node", "server.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3aba7cc..65cf36c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,43 +1,66 @@ -version: '3' +# Local full-stack dev compose. +# +# `docker compose up --build` boots the Next.js + Payload app next to a +# dedicated Postgres and mounts `./media` as the Payload upload volume, so +# you get the exact same shape as the Coolify deployment without needing a +# running Coolify cluster. +# +# In production (Coolify) we DO NOT use this file — Coolify builds the +# Dockerfile directly per environment and provisions its own managed +# PostgreSQL service. DATABASE_URI is injected as an env var per +# environment by Coolify. See DEPLOYMENT.md. services: - payload: - image: node:18-alpine - ports: - - '3000:3000' - volumes: - - .:/home/node/app - - node_modules:/home/node/app/node_modules - working_dir: /home/node/app/ - command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev" - depends_on: - - mongo - # - postgres + app: + build: + context: . + dockerfile: Dockerfile + image: polaris-task-force:local + container_name: polaris-task-force-app + # In Coolify these come from the per-environment env vars configured in + # the Coolify UI; here we read a local .env (see .env.example). env_file: - .env - - # Ensure your DATABASE_URI uses 'mongo' as the hostname ie. mongodb://mongo/my-db-name - mongo: - image: mongo:latest + environment: + # Override the host DB hostname with the compose service name so + # the app reaches the bundled postgres instead of localhost. + # DATABASE_URI is rewritten below — strip any user-provided value. + NODE_ENV: production + DATABASE_URI: postgres://${POSTGRES_USER:-ptfapp}:${POSTGRES_PASSWORD:-polaristaskforcedb}@postgres:5432/${POSTGRES_DB:-ptf-app-dev} ports: - - '27017:27017' - command: - - --storageEngine=wiredTiger + - "${APP_PORT:-3000}:3000" volumes: - - data:/data/db - logging: - driver: none + # Persist Payload uploads between rebuilds locally. + - ./media:/app/media + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/api/health"] + interval: 30s + timeout: 10s + start_period: 60s + retries: 3 + restart: unless-stopped - # Uncomment the following to use postgres - # postgres: - # restart: always - # image: postgres:latest - # volumes: - # - pgdata:/var/lib/postgresql/data - # ports: - # - "5432:5432" + postgres: + image: postgres:17-alpine + container_name: polaris-task-force-pg + environment: + POSTGRES_USER: ${POSTGRES_USER:-ptfapp} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-polaristaskforcedb} + POSTGRES_DB: ${POSTGRES_DB:-ptf-app-dev} + volumes: + - pgdata:/var/lib/postgresql/data + ports: + - "${POSTGRES_PORT:-5432}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-ptfapp} -d ${POSTGRES_DB:-ptf-app-dev}"] + interval: 10s + timeout: 5s + start_period: 10s + retries: 5 + restart: unless-stopped volumes: - data: - # pgdata: - node_modules: + pgdata: \ No newline at end of file