chore(deploy): add Coolify-ready Dockerfile and compose stack
This commit is contained in:
parent
3597833843
commit
c08015ed9f
4 changed files with 280 additions and 104 deletions
69
.dockerignore
Normal file
69
.dockerignore
Normal file
|
|
@ -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
|
||||||
64
.env.example
64
.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
|
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
|
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
|
GAME_TICK_NOTIFY_SECRET=YOUR_GAME_TICK_NOTIFY_SECRET
|
||||||
|
|
||||||
# --- Discord bot ---
|
# --- Payload email (nodemailer / SMTP relay) ---
|
||||||
# Bot token from the Discord Developer Portal. Required to run the bot.
|
# Optional. Leave blank to disable outbound email (password resets, etc).
|
||||||
DISCORD_TOKEN=
|
EMAIL_FROM_ADDRESS=
|
||||||
# Server (guild) id that slash commands are registered to. Required.
|
EMAIL_FROM_NAME=
|
||||||
DISCORD_GUILD_ID=
|
EMAIL_HOST=
|
||||||
# Channel id where attendance embeds are posted.
|
EMAIL_PORT=587
|
||||||
DISCORD_OPS_CHANNEL_ID=
|
EMAIL_USERNAME=
|
||||||
# Channel id used as the default /announce target.
|
EMAIL_PASSWORD=
|
||||||
DISCORD_ANNOUNCE_CHANNEL_ID=
|
|
||||||
# Comma-separated Discord role ids that grant staff permissions.
|
# --- Docker compose tunables (local dev only — not used by Coolify) ---
|
||||||
DISCORD_STAFF_ROLE_IDS=
|
POSTGRES_USER=ptfapp
|
||||||
# How often the attendance reconcile loop runs, in ms. Defaults to 60000.
|
POSTGRES_PASSWORD=polaristaskforcedb
|
||||||
DISCORD_ATTENDANCE_POLL_MS=
|
POSTGRES_DB=ptf-app-dev
|
||||||
# How often the notification bridge polls for new notifications, in ms. Defaults to 20000.
|
POSTGRES_PORT=5432
|
||||||
DISCORD_NOTIFICATION_POLL_MS=
|
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=
|
||||||
160
Dockerfile
160
Dockerfile
|
|
@ -1,71 +1,129 @@
|
||||||
# To use this Dockerfile, you have to set `output: 'standalone'` in your next.config.mjs file.
|
# syntax=docker/dockerfile:1.7
|
||||||
# From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
|
#
|
||||||
|
# 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 <bin>` 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
|
# ────────────────────────── prod-deps ────────────────────────────
|
||||||
FROM base AS deps
|
# Production-only install. Used at runtime alongside the standalone
|
||||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
# server so the Payload CLI / migrations / bin scripts have every
|
||||||
RUN apk add --no-cache libc6-compat
|
# 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
|
WORKDIR /app
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install --production --frozen-lockfile
|
||||||
|
|
||||||
# Install dependencies based on the preferred package manager
|
# ─────────────────────────── builder ─────────────────────────────
|
||||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
# Full install (incl. devDeps) + Next.js standalone build.
|
||||||
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
|
|
||||||
FROM base AS builder
|
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
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
COPY . .
|
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.
|
# ─────────────────────────── runner ──────────────────────────────
|
||||||
# Learn more here: https://nextjs.org/telemetry
|
# Final production image: Next standalone runtime + full prod deps
|
||||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
# (for payload bins) + src/migrations (for `bun run payload <bin>`).
|
||||||
# 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
|
|
||||||
FROM base AS runner
|
FROM base AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV=production \
|
||||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
NEXT_TELEMETRY_DISABLED=1 \
|
||||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
PORT=3000 \
|
||||||
|
HOSTNAME=0.0.0.0 \
|
||||||
|
PNPM_HOME=/app
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
# The `node` user already exists in `node:*-alpine` images.
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN mkdir -p /app/media \
|
||||||
|
&& chown -R node:node /app
|
||||||
|
|
||||||
# Remove this line if you do not have this folder
|
# 1) Next.js standalone runtime (server.js + traced node_modules + .next).
|
||||||
COPY --from=builder /app/public ./public
|
# `.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
|
# 4) Overlay the FULL production node_modules on top of the pruned
|
||||||
RUN mkdir .next
|
# standalone one. Docker COPY merges directories file-by-file
|
||||||
RUN chown nextjs:nodejs .next
|
# (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
|
# 5) Source + top-level config so `bun run payload <bin>` and
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
# `payload migrate` work via `docker exec`. These read
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
# @payload-config (alias in tsconfig.json) and the Payload
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
# 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
|
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
|
USER node
|
||||||
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
|
||||||
CMD HOSTNAME="0.0.0.0" node server.js
|
# 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"]
|
||||||
|
|
@ -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:
|
services:
|
||||||
payload:
|
app:
|
||||||
image: node:18-alpine
|
build:
|
||||||
ports:
|
context: .
|
||||||
- '3000:3000'
|
dockerfile: Dockerfile
|
||||||
volumes:
|
image: polaris-task-force:local
|
||||||
- .:/home/node/app
|
container_name: polaris-task-force-app
|
||||||
- node_modules:/home/node/app/node_modules
|
# In Coolify these come from the per-environment env vars configured in
|
||||||
working_dir: /home/node/app/
|
# the Coolify UI; here we read a local .env (see .env.example).
|
||||||
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev"
|
|
||||||
depends_on:
|
|
||||||
- mongo
|
|
||||||
# - postgres
|
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
environment:
|
||||||
# Ensure your DATABASE_URI uses 'mongo' as the hostname ie. mongodb://mongo/my-db-name
|
# Override the host DB hostname with the compose service name so
|
||||||
mongo:
|
# the app reaches the bundled postgres instead of localhost.
|
||||||
image: mongo:latest
|
# 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:
|
ports:
|
||||||
- '27017:27017'
|
- "${APP_PORT:-3000}:3000"
|
||||||
command:
|
|
||||||
- --storageEngine=wiredTiger
|
|
||||||
volumes:
|
volumes:
|
||||||
- data:/data/db
|
# Persist Payload uploads between rebuilds locally.
|
||||||
logging:
|
- ./media:/app/media
|
||||||
driver: none
|
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:
|
||||||
# postgres:
|
image: postgres:17-alpine
|
||||||
# restart: always
|
container_name: polaris-task-force-pg
|
||||||
# image: postgres:latest
|
environment:
|
||||||
# volumes:
|
POSTGRES_USER: ${POSTGRES_USER:-ptfapp}
|
||||||
# - pgdata:/var/lib/postgresql/data
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-polaristaskforcedb}
|
||||||
# ports:
|
POSTGRES_DB: ${POSTGRES_DB:-ptf-app-dev}
|
||||||
# - "5432:5432"
|
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:
|
volumes:
|
||||||
data:
|
pgdata:
|
||||||
# pgdata:
|
|
||||||
node_modules:
|
|
||||||
Loading…
Reference in a new issue