Normalize every submission to mono 96 kbps MP3 so takes play everywhere regardless of the recording codec; falls back to storing the original file when ffmpeg is unavailable or fails. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
199 lines
9.9 KiB
Docker
199 lines
9.9 KiB
Docker
# 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 is build-time ONLY (install deps + run the Next build on the
|
|
# build server). The runner image is Node-only: the production host has an
|
|
# older CPU without AVX/AVX2 — which Bun >= 1.2 requires — so `bun` can
|
|
# never execute there. Runtime uses plain Node for everything:
|
|
# - web server: node server.js (Next standalone)
|
|
# - payload bins: node node_modules/payload/bin.js <cmd> (game-tick /
|
|
# market-tick / migrate; the payload CLI transpiles the TS config itself)
|
|
# - Discord bot: node --import tsx src/bot/index.ts
|
|
#
|
|
# See DEPLOYMENT.md for the full Coolify workflow (per-env Postgres,
|
|
# env vars, scheduled jobs, persistent media volume, rollback).
|
|
|
|
# ───────────────────────────── base ─────────────────────────────
|
|
# Node 22 alpine + Bun, shared by all three stages.
|
|
FROM node:22-alpine AS base
|
|
RUN apk add --no-cache libc6-compat curl wget bash
|
|
SHELL ["/bin/bash", "-c"]
|
|
|
|
# Bun 1.3.11, installed to /usr/local/bin so it is on PATH for the BUILD
|
|
# stages only (base -> prod-deps -> builder). The runner stage is NOT built
|
|
# from this base — it uses plain node:22-alpine because the production host
|
|
# CPU lacks AVX/AVX2 (required by Bun >= 1.2) and Bun must never execute
|
|
# there. Two gotchas this setup avoids:
|
|
# - `ARG PATH=...` does NOT set PATH in RUN environments — ARG values are
|
|
# only substituted into build instructions, never injected as env vars
|
|
# (that produced "bun: command not found" in the prod-deps stage).
|
|
# - `$HOME` is empty during `docker build`, so the installer's default
|
|
# `$HOME/.bun` would resolve to `/.bun` — off-PATH and root-owned.
|
|
ENV BUN_INSTALL=/usr/local
|
|
RUN curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.11" \
|
|
&& bun --version
|
|
|
|
# ────────────────────────── 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 ./
|
|
# Retry with a fresh cache dir per attempt: a corrupt tarball download
|
|
# (flaky registry/proxy → "Integrity check failed for tarball: drizzle-kit")
|
|
# must never be reused from a bun cache, across attempts or cached layers.
|
|
RUN for i in 1 2 3; do \
|
|
rm -rf /tmp/bun-cache; \
|
|
if bun install --production --frozen-lockfile --cache-dir=/tmp/bun-cache; then \
|
|
exit 0; \
|
|
fi; \
|
|
echo "bun install (production) attempt $i/3 failed, retrying in 5s..."; \
|
|
sleep 5; \
|
|
done; \
|
|
echo "bun install (production) failed after 3 attempts"; \
|
|
exit 1
|
|
|
|
# ─────────────────────────── 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. It
|
|
# is the LOCAL-build fallback: Coolify strips `.git` from the build context,
|
|
# so there the script reads the commit SHA from the SOURCE_COMMIT build arg
|
|
# instead (see the ARG/ENV block above `RUN bun run build`). Not carried into
|
|
# the runner image.
|
|
RUN apk add --no-cache git
|
|
WORKDIR /app
|
|
COPY package.json bun.lock ./
|
|
# Same retry + fresh-cache pattern as prod-deps (corrupt tarball downloads
|
|
# fail bun's integrity check; a fresh --cache-dir per attempt prevents reuse).
|
|
RUN for i in 1 2 3; do \
|
|
rm -rf /tmp/bun-cache; \
|
|
if bun install --frozen-lockfile --cache-dir=/tmp/bun-cache; then \
|
|
exit 0; \
|
|
fi; \
|
|
echo "bun install attempt $i/3 failed, retrying in 5s..."; \
|
|
sleep 5; \
|
|
done; \
|
|
echo "bun install failed after 3 attempts"; \
|
|
exit 1
|
|
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
|
|
# Git metadata passthrough for `generate:version-info` (the script reads env
|
|
# vars first). Coolify strips `.git` from the build context, so builds there
|
|
# must get the commit SHA via build args: Coolify injects `ARG
|
|
# SOURCE_COMMIT=<sha>` into the Dockerfile after each FROM only when "Include
|
|
# Source Commit in Build" is enabled (application settings → Advanced) and
|
|
# "Inject Build Args to Dockerfile" is on (default). The bare `ARG`
|
|
# declarations below preserve that injected value (no `=default` reset), and
|
|
# the `ENV` forwards it to `bun run build`. The GIT_* args are optional
|
|
# overrides for other build environments; none of this ships to runtime.
|
|
ARG SOURCE_COMMIT
|
|
ARG GIT_TAG
|
|
ARG GIT_DESCRIBE
|
|
ARG GIT_COMMIT_HASH
|
|
ARG GIT_COMMIT_DATE
|
|
ARG GIT_COMMIT_SUBJECT
|
|
ENV SOURCE_COMMIT=$SOURCE_COMMIT \
|
|
GIT_TAG=$GIT_TAG \
|
|
GIT_DESCRIBE=$GIT_DESCRIBE \
|
|
GIT_COMMIT_HASH=$GIT_COMMIT_HASH \
|
|
GIT_COMMIT_DATE=$GIT_COMMIT_DATE \
|
|
GIT_COMMIT_SUBJECT=$GIT_COMMIT_SUBJECT
|
|
RUN bun run build
|
|
|
|
# ─────────────────────────── runner ──────────────────────────────
|
|
# Final production image: Next standalone runtime + full prod deps +
|
|
# src/migrations. Node-only (see header): the production host CPU has no
|
|
# AVX/AVX2, so Bun cannot run there. Payload bins run via
|
|
# `node node_modules/payload/bin.js <cmd>`; the Discord bot via
|
|
# `node --import tsx src/bot/index.ts` (tsx resolves the @/ + @payload-config
|
|
# tsconfig path aliases that plain Node type-stripping cannot).
|
|
FROM node:22-alpine AS runner
|
|
# ffmpeg: voiceover submissions are transcoded to MP3 in submitVoiceRecording.
|
|
RUN apk add --no-cache libc6-compat wget ffmpeg
|
|
WORKDIR /app
|
|
|
|
ENV NODE_ENV=production \
|
|
NEXT_TELEMETRY_DISABLED=1 \
|
|
PORT=3000 \
|
|
HOSTNAME=0.0.0.0
|
|
|
|
# The `node` user already exists in `node:*-alpine` images.
|
|
RUN mkdir -p /app/media \
|
|
&& chown -R node:node /app
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# 5) Source + top-level config so `bun run payload <bin>` 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
|
|
|
|
# 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
|
|
|
|
# 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=15s --retries=3 \
|
|
CMD wget -qO- "http://127.0.0.1:${PORT:-3000}/api/health" >/dev/null 2>&1 || exit 1
|
|
|
|
COPY --chmod=755 docker-entrypoint.sh /docker-entrypoint.sh
|
|
|
|
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).
|
|
ENTRYPOINT ["/bin/sh", "/docker-entrypoint.sh"]
|
|
CMD ["node", "server.js"]
|