chore(deploy): mount a persistent tiles volume and refresh deploy docs
Create /app/public/tiles in the runner image, bind-mount it in compose, and document the Coolify volume plus upload steps; restructure .gitignore (test artifacts, /public/tiles, backups). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
8135c9850b
commit
8cb6590ce6
5 changed files with 119 additions and 108 deletions
110
.gitignore
vendored
110
.gitignore
vendored
|
|
@ -1,74 +1,58 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
|
/.archived/playwright-report/
|
||||||
|
/.archived/test-results/
|
||||||
|
/backups/
|
||||||
|
# bin script file logs (game-tick, market-tick, mission-tick, generate-mission)
|
||||||
|
/blob-report/
|
||||||
|
/build
|
||||||
|
/coverage
|
||||||
|
# debug
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
.DS_Store
|
||||||
/.pnp
|
.env
|
||||||
.pnp.js
|
.env.*
|
||||||
.yarn/install-state.gz
|
.env.development
|
||||||
|
.env*.local
|
||||||
|
.env.prd
|
||||||
|
.env.stg
|
||||||
|
# generated version info (regenerated by predev/prebuild/postversion)
|
||||||
/.idea/*
|
/.idea/*
|
||||||
!/.idea/runConfigurations
|
!/.idea/runConfigurations
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# generated version info (regenerated by predev/prebuild/postversion)
|
|
||||||
/src/generated/
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# bin script file logs (game-tick, market-tick, mission-tick, generate-mission)
|
|
||||||
/logs/
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
||||||
|
|
||||||
.env
|
|
||||||
.env.stg
|
|
||||||
.env.prd
|
|
||||||
.env.development
|
|
||||||
.env.*
|
|
||||||
|
|
||||||
/media
|
|
||||||
/backups/
|
|
||||||
|
|
||||||
# Playwright
|
|
||||||
node_modules/
|
|
||||||
/test-results/
|
|
||||||
/playwright-report/
|
|
||||||
/.archived/test-results/
|
|
||||||
/.archived/playwright-report/
|
|
||||||
/blob-report/
|
|
||||||
/playwright/.cache/
|
|
||||||
|
|
||||||
# local tooling
|
# local tooling
|
||||||
/.logs/
|
/.logs/
|
||||||
|
/logs/
|
||||||
|
/media
|
||||||
|
# misc
|
||||||
|
/.next/
|
||||||
|
next-env.d.ts
|
||||||
|
# next.js
|
||||||
|
/node_modules
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
/.omo/
|
/.omo/
|
||||||
/.opencode/
|
|
||||||
/.openchamber/
|
/.openchamber/
|
||||||
/.playwright-mcp/
|
/.opencode/
|
||||||
|
/out/
|
||||||
|
*.pem
|
||||||
# personal shell alias notes
|
# personal shell alias notes
|
||||||
|
# Playwright
|
||||||
|
/playwright/.cache/
|
||||||
|
/.playwright-mcp/
|
||||||
|
/playwright-report/
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
# production
|
||||||
|
/public/tiles
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
/src/generated/
|
||||||
|
# testing
|
||||||
|
/test-results/
|
||||||
|
*.tsbuildinfo
|
||||||
|
# typescript
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.yarn/install-state.gz
|
||||||
.zinc
|
.zinc
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,9 @@ Known pre-existing errors (not yours, don't widen scope to fix them): `src/colle
|
||||||
|
|
||||||
## Test details
|
## Test details
|
||||||
|
|
||||||
- **Integration tests**: `tests/int/**/*.int.spec.ts` — Vitest with jsdom. Requires a live PostgreSQL database (connection from `.env`). Uses `dotenv/config` via `vitest.setup.ts`.
|
- **Dedicated test database**: all tests run against `<db>_test` (derived from `DATABASE_URI`, or set `TEST_DATABASE_URI`), never the dev database. `tests/test-db.ts` bootstraps it idempotently: ensure database exists, push the schema (drizzle `push`, because the migration chain assumes a schema that push created), truncate every table (migration journals and PostGIS internals survive), seed baselines (`seedRoles` + a Game Rules global with a main currency resource, `tests/seed-baseline.ts`), and repair id sequences (migrations backfill explicit-id rows, which leaves sequences behind and causes `ValidationError: field is invalid: id` on creates). Truncate runs at both global setup (clears crashed-run leftovers) and teardown, so every run starts and ends empty. `TEST_DB_HARD_RESET=1` drops and recreates the test database from scratch instead of truncating. Serialized test files (`fileParallelism: false`) because parallel workers share the one database.
|
||||||
- **E2E tests**: `tests/e2e/*.e2e.spec.ts` — Playwright with a project named "vivaldi" that launches the **Vivaldi binary** (`/usr/bin/vivaldi`, headless args in `playwright.config.ts`) — not plain Chromium. The `webServer` config auto-starts `pnpm dev` (with `reuseExistingServer: true`). Currently minimal (homepage smoke test).
|
- **Integration tests**: `tests/int/**/*.int.spec.ts` — Vitest with jsdom. Requires the live PostgreSQL test database (bootstrapped automatically via `vitest-global.ts`; `vitest.setup.ts` points the process at it). Uses `dotenv/config` via `vitest.setup.ts`.
|
||||||
|
- **E2E tests**: `tests/e2e/*.e2e.spec.ts` — Playwright with a project named "vivaldi" that launches the **Vivaldi binary** (`/usr/bin/vivaldi`, headless args in `playwright.config.ts`) — not plain Chromium. The `webServer` config auto-starts `pnpm dev` with `DATABASE_URI` pointed at the test DB (bootstrapped by `tests/e2e/global-setup.ts`, with `reuseExistingServer: true`). Currently minimal (homepage smoke test). Caveat: when your own dev server is already on port 3000, `reuseExistingServer` silently reuses it and its DEV database, bypassing the test-DB wiring; free the port first for a true test-DB run.
|
||||||
- Run a single integration test: `bun run vitest run tests/int/api.int.spec.ts`
|
- Run a single integration test: `bun run vitest run tests/int/api.int.spec.ts`
|
||||||
- Run a single e2e test: `bun run playwright test tests/e2e/frontend.e2e.spec.ts`
|
- Run a single e2e test: `bun run playwright test tests/e2e/frontend.e2e.spec.ts`
|
||||||
|
|
||||||
|
|
@ -415,6 +416,8 @@ Whenever creating a new component or refactoring an existing component's styling
|
||||||
|
|
||||||
`bun run deploy` bumps the patch version (via `bun pm version patch`) and runs the production build. Push the resulting version commit to deploy through Coolify; the legacy `build/deploy.sh` path is no longer used.
|
`bun run deploy` bumps the patch version (via `bun pm version patch`) and runs the production build. Push the resulting version commit to deploy through Coolify; the legacy `build/deploy.sh` path is no longer used.
|
||||||
|
|
||||||
|
**Volumes**: prod containers mount persistent volumes for `/app/media` (uploads) and `/app/public/tiles` (gitignored map tile pyramids, uploaded to the server by hand). Both dirs are created in the image; see DEPLOYMENT.md Sections 4 and 8.
|
||||||
|
|
||||||
**Pre-deploy dash gate**: before any production build, release version bump, or deploy, run the `dash-sanitize` skill (`/home/jason/.agents/skills/dash-sanitize/SKILL.md`) and show its verification output (re-scan classification + filtered tsc) before proceeding. A dirty user-facing dash scan pauses the release; a clean re-scan on an already-swept tree takes seconds and should still be run rather than assumed.
|
**Pre-deploy dash gate**: before any production build, release version bump, or deploy, run the `dash-sanitize` skill (`/home/jason/.agents/skills/dash-sanitize/SKILL.md`) and show its verification output (re-scan classification + filtered tsc) before proceeding. A dirty user-facing dash scan pauses the release; a clean re-scan on an already-swept tree takes seconds and should still be run rather than assumed.
|
||||||
|
|
||||||
**Version bumps and release tags: `main` deploys only**: whenever a `/git` or `/git-master` workflow lands work on `main` (or a deploy is being cut), bump the version with `bun pm version patch --message "v%s - <descriptor>"` and push it together with the work. `<descriptor>` is a short comma-separated summary of what the release ships (derived from the commits just created), e.g. `v0.2.4 - add voicelines, update git rules` (never a bare version number). The command itself creates the `vX.Y.Z` version commit **and** a local git tag. `push.followTags` is set in this repo, so a plain `git push` automatically carries the annotated release tag with the commits, but never use a bare `git push --tags`, which sends only tags and not the branch. **Feature branches must never bump versions or push release tags**: parallel branches bumping the same semver line desync `package.json` from the tags that actually deploy, and at merge time the version history no longer matches what shipped. Release tags exist solely to mark `main` deploys. The deployed site's version overlay reads `package.json`'s `version` (Coolify builds have no git metadata, so the displayed version is `<semver>+<commit sha>`), and an unbumped semver means every deploy shows the same `0.2.0`-style number even though the commit-hash suffix changes. Skipping the bump on a `main` deploy (or pushing the deploy commit without its tag) makes releases indistinguishable and breaks version history.
|
**Version bumps and release tags: `main` deploys only**: whenever a `/git` or `/git-master` workflow lands work on `main` (or a deploy is being cut), bump the version with `bun pm version patch --message "v%s - <descriptor>"` and push it together with the work. `<descriptor>` is a short comma-separated summary of what the release ships (derived from the commits just created), e.g. `v0.2.4 - add voicelines, update git rules` (never a bare version number). The command itself creates the `vX.Y.Z` version commit **and** a local git tag. `push.followTags` is set in this repo, so a plain `git push` automatically carries the annotated release tag with the commits, but never use a bare `git push --tags`, which sends only tags and not the branch. **Feature branches must never bump versions or push release tags**: parallel branches bumping the same semver line desync `package.json` from the tags that actually deploy, and at merge time the version history no longer matches what shipped. Release tags exist solely to mark `main` deploys. The deployed site's version overlay reads `package.json`'s `version` (Coolify builds have no git metadata, so the displayed version is `<semver>+<commit sha>`), and an unbumped semver means every deploy shows the same `0.2.0`-style number even though the commit-hash suffix changes. Skipping the bump on a `main` deploy (or pushing the deploy commit without its tag) makes releases indistinguishable and breaks version history.
|
||||||
|
|
|
||||||
104
DEPLOYMENT.md
104
DEPLOYMENT.md
|
|
@ -17,7 +17,7 @@ Local full-stack dev with the same image shape is described at the end (`docker
|
||||||
│ │ (Docker- │ │ svc │ per-env) │
|
│ │ (Docker- │ │ svc │ per-env) │
|
||||||
│ │ file) │ │ + volume │ │
|
│ │ file) │ │ + volume │ │
|
||||||
│ │ + media │ └─────────────┘ │
|
│ │ + media │ └─────────────┘ │
|
||||||
│ │ volume │ │
|
│ │ volume │ │
|
||||||
│ └─────┬──────┘ │
|
│ └─────┬──────┘ │
|
||||||
│ │ scheduled jobs (docker exec → node payload bin) │
|
│ │ scheduled jobs (docker exec → node payload bin) │
|
||||||
└────────┼──────────────────────────────────────────────────┘
|
└────────┼──────────────────────────────────────────────────┘
|
||||||
|
|
@ -26,12 +26,12 @@ Local full-stack dev with the same image shape is described at the end (`docker
|
||||||
```
|
```
|
||||||
|
|
||||||
- **One image**, parameterized by env vars per environment. No image bake per env required.
|
- **One image**, parameterized by env vars per environment. No image bake per env required.
|
||||||
- **PostgreSQL** is provisioned by Coolify as a *separate service* in each environment (`dev`, `stg`, `prd`). The app talks to it via `DATABASE_URI` injected by Coolify.
|
- **PostgreSQL** is provisioned by Coolify as a _separate service_ in each environment (`dev`, `stg`, `prd`). The app talks to it via `DATABASE_URI` injected by Coolify.
|
||||||
- **Payload uploads** (`media/` collection) are written to a Coolify **Persistent Storage Volume** mounted at `/app/media` in the app container, so uploads survive container restarts, rollbacks, and rebuilds.
|
- **Payload uploads** (`media/` collection) are written to a Coolify **Persistent Storage Volume** mounted at `/app/media` in the app container, so uploads survive container restarts, rollbacks, and rebuilds.
|
||||||
- **Scheduled one-shot jobs** (game-tick, market-tick) run via `docker exec` Coolify Scheduled Tasks against the running app container — same image, same env, no second service required.
|
- **Scheduled one-shot jobs** (game-tick, market-tick) run via `docker exec` Coolify Scheduled Tasks against the running app container — same image, same env, no second service required.
|
||||||
- **The Discord bot ships inside the image** (`src/bot/`) and activates per-environment when `DISCORD_TOKEN` is set — prod only. It shares the same Postgres DB via the container env and inherits the single-instance constraint: **2 replicas = two bot instances** (duplicate command registration, double embeds/DMs). Notifications generated while the container is down are **not backfilled** (the Discord notification bridge polls with an in-memory cursor).
|
- **The Discord bot ships inside the image** (`src/bot/`) and activates per-environment when `DISCORD_TOKEN` is set — prod only. It shares the same Postgres DB via the container env and inherits the single-instance constraint: **2 replicas = two bot instances** (duplicate command registration, double embeds/DMs). Notifications generated while the container is down are **not backfilled** (the Discord notification bridge polls with an in-memory cursor).
|
||||||
|
|
||||||
> ⚠️ **Single-instance only.** The realtime SSE pipeline (`gameTick → /api/game-tick/notify → in-process bus → SSE /api/realtime → client router.refresh()`) uses an **in-memory subscriber bus** (`src/lib/realtime/bus.ts`) that is not shared across processes. Coolify *must* run **exactly 1 replica** of the app container per environment for SSE to work. If you scale the app horizontally, SSE stops working (live shipment/event log updates silently fail). Everything else continues to work; you'll need a distributed bus before you can scale. See `src/lib/realtime/bus.ts` and `AGENTS.md` for context.
|
> ⚠️ **Single-instance only.** The realtime SSE pipeline (`gameTick → /api/game-tick/notify → in-process bus → SSE /api/realtime → client router.refresh()`) uses an **in-memory subscriber bus** (`src/lib/realtime/bus.ts`) that is not shared across processes. Coolify _must_ run **exactly 1 replica** of the app container per environment for SSE to work. If you scale the app horizontally, SSE stops working (live shipment/event log updates silently fail). Everything else continues to work; you'll need a distributed bus before you can scale. See `src/lib/realtime/bus.ts` and `AGENTS.md` for context.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -67,7 +67,8 @@ For each environment (`dev`, `stg`, `prd`):
|
||||||
- **Don't** use the `docker-compose.yml` for the production app service — that file is for local dev (it bundles its own Postgres). Coolify manages the Postgres service for you.
|
- **Don't** use the `docker-compose.yml` for the production app service — that file is for local dev (it bundles its own Postgres). Coolify manages the Postgres service for you.
|
||||||
3. Set the environment variables (Section 5).
|
3. Set the environment variables (Section 5).
|
||||||
4. Attach a **Persistent Storage** entry mapping `/app/media` (read/write). This is where Payload stores uploaded media. The directory is created and chowned inside the image; just mount the volume on top.
|
4. Attach a **Persistent Storage** entry mapping `/app/media` (read/write). This is where Payload stores uploaded media. The directory is created and chowned inside the image; just mount the volume on top.
|
||||||
5. Deploy. First build will take a few minutes (Bun install + Next.js standalone build, `--max-old-space-size=8000` — make sure the build host has ≥ 4 GB free RAM, ideally 8).
|
5. Attach a second **Persistent Storage** entry mapping `/app/public/tiles` (read/write) if you serve map tile pyramids. The directory is created and chowned inside the image; upload tile sets into the volume (see Section 8).
|
||||||
|
6. Deploy. First build will take a few minutes (Bun install + Next.js standalone build, `--max-old-space-size=8000` — make sure the build host has ≥ 4 GB free RAM, ideally 8).
|
||||||
|
|
||||||
### Build args you may want to override
|
### Build args you may want to override
|
||||||
|
|
||||||
|
|
@ -77,25 +78,25 @@ The Dockerfile pins Bun 1.3.11 (**build-time only**) and Node 22 alpine (runtime
|
||||||
|
|
||||||
## 5. Environment variables (per environment)
|
## 5. Environment variables (per environment)
|
||||||
|
|
||||||
| Var | Required | Example / Notes |
|
| Var | Required | Example / Notes |
|
||||||
|---|---|---|
|
| ------------------------------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `DATABASE_URI` | ✅ | Coolify auto-injects this from the per-env Postgres service if linked. Format: `postgres://<user>:<pwd>@<host>:5432/<db>` |
|
| `DATABASE_URI` | ✅ | Coolify auto-injects this from the per-env Postgres service if linked. Format: `postgres://<user>:<pwd>@<host>:5432/<db>` |
|
||||||
| `PAYLOAD_SECRET` | ✅ | Random 32+ chars. `openssl rand -hex 16`. **Unique per environment.** |
|
| `PAYLOAD_SECRET` | ✅ | Random 32+ chars. `openssl rand -hex 16`. **Unique per environment.** |
|
||||||
| `APP_URL` | ✅ | Public base URL for this environment, e.g. `https://dev.ptf.example.com` |
|
| `APP_URL` | ✅ | Public base URL for this environment, e.g. `https://dev.ptf.example.com` |
|
||||||
| `GAME_TICK_NOTIFY_SECRET` | ✅ | Shared secret for `/api/game-tick/notify`. **Must match** the value used by your scheduled job (Sec. 6). |
|
| `GAME_TICK_NOTIFY_SECRET` | ✅ | Shared secret for `/api/game-tick/notify`. **Must match** the value used by your scheduled job (Sec. 6). |
|
||||||
| `EMAIL_FROM_ADDRESS` | — | Outbound email "from" address (e.g. `arma@onyxsimple.com`) |
|
| `EMAIL_FROM_ADDRESS` | — | Outbound email "from" address (e.g. `arma@onyxsimple.com`) |
|
||||||
| `EMAIL_FROM_NAME` | — | Display name (e.g. `Arma`) |
|
| `EMAIL_FROM_NAME` | — | Display name (e.g. `Arma`) |
|
||||||
| `EMAIL_HOST` | — | SMTP host (e.g. `smtp-relay.brevo.com`) |
|
| `EMAIL_HOST` | — | SMTP host (e.g. `smtp-relay.brevo.com`) |
|
||||||
| `EMAIL_PORT` | — | `587` |
|
| `EMAIL_PORT` | — | `587` |
|
||||||
| `EMAIL_USERNAME` | — | SMTP user |
|
| `EMAIL_USERNAME` | — | SMTP user |
|
||||||
| `EMAIL_PASSWORD` | — | SMTP password / API key |
|
| `EMAIL_PASSWORD` | — | SMTP password / API key |
|
||||||
| `DISCORD_TOKEN` | iff bot enabled in this env | Set in **prod only** — presence activates the bot (activation gate below). |
|
| `DISCORD_TOKEN` | iff bot enabled in this env | Set in **prod only** — presence activates the bot (activation gate below). |
|
||||||
| `DISCORD_GUILD_ID` | iff `DISCORD_TOKEN` set | Config throws at import if the token is set and this is missing. |
|
| `DISCORD_GUILD_ID` | iff `DISCORD_TOKEN` set | Config throws at import if the token is set and this is missing. |
|
||||||
| `DISCORD_OPS_CHANNEL_ID` | — | Ops channel for attendance RSVP embeds. |
|
| `DISCORD_OPS_CHANNEL_ID` | — | Ops channel for attendance RSVP embeds. |
|
||||||
| `DISCORD_ANNOUNCE_CHANNEL_ID` | — | Channel for `/announce`. |
|
| `DISCORD_ANNOUNCE_CHANNEL_ID` | — | Channel for `/announce`. |
|
||||||
| `DISCORD_STAFF_ROLE_IDS` | — | Comma-separated staff role IDs (staff-only `/announce`). |
|
| `DISCORD_STAFF_ROLE_IDS` | — | Comma-separated staff role IDs (staff-only `/announce`). |
|
||||||
| `DISCORD_ATTENDANCE_POLL_MS` | — | Attendance reconcile poll interval; default `60000`. |
|
| `DISCORD_ATTENDANCE_POLL_MS` | — | Attendance reconcile poll interval; default `60000`. |
|
||||||
| `DISCORD_NOTIFICATION_POLL_MS` | — | Notification bridge poll interval; default `20000`. |
|
| `DISCORD_NOTIFICATION_POLL_MS` | — | Notification bridge poll interval; default `20000`. |
|
||||||
|
|
||||||
> Never copy the dev `PAYLOAD_SECRET` into prod. Never copy `GAME_TICK_NOTIFY_SECRET`. Generate fresh per environment.
|
> Never copy the dev `PAYLOAD_SECRET` into prod. Never copy `GAME_TICK_NOTIFY_SECRET`. Generate fresh per environment.
|
||||||
|
|
||||||
|
|
@ -121,13 +122,13 @@ The `game-tick` and `market-tick` Payload bins are registered on `payload.config
|
||||||
|
|
||||||
### Option A — Coolify Scheduled Tasks (recommended)
|
### Option A — Coolify Scheduled Tasks (recommended)
|
||||||
|
|
||||||
Coolify has a **Scheduled Tasks** feature per service (`<service> → Scheduled Tasks → Add`). Each scheduled task runs a command inside the *running service container* — equivalent to `docker exec`. Add:
|
Coolify has a **Scheduled Tasks** feature per service (`<service> → Scheduled Tasks → Add`). Each scheduled task runs a command inside the _running service container_ — equivalent to `docker exec`. Add:
|
||||||
|
|
||||||
| Job | Schedule (cron) | Command |
|
| Job | Schedule (cron) | Command |
|
||||||
|---|---|---|
|
| ----------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
||||||
| Process shipments | `*/5 * * * *` (every 5 min, tune to match `gameTickIntervalMinutes` in your Game Rules global) | `node node_modules/payload/bin.js game-tick` |
|
| Process shipments | `*/5 * * * *` (every 5 min, tune to match `gameTickIntervalMinutes` in your Game Rules global) | `node node_modules/payload/bin.js game-tick` |
|
||||||
| Refresh market | `*/10 * * * *` (every 10 min — adjust as needed) | `node node_modules/payload/bin.js market-tick` |
|
| Refresh market | `*/10 * * * *` (every 10 min — adjust as needed) | `node node_modules/payload/bin.js market-tick` |
|
||||||
| Server presence | `* * * * *` (every minute) | `node node_modules/payload/bin.js server-tick` |
|
| Server presence | `* * * * *` (every minute) | `node node_modules/payload/bin.js server-tick` |
|
||||||
|
|
||||||
The commands run inside the app container with the app's env (incl. `DATABASE_URI`, `APP_URL`, `GAME_TICK_NOTIFY_SECRET`), so the bins talk to the same DB and post the notify callback correctly.
|
The commands run inside the app container with the app's env (incl. `DATABASE_URI`, `APP_URL`, `GAME_TICK_NOTIFY_SECRET`), so the bins talk to the same DB and post the notify callback correctly.
|
||||||
|
|
||||||
|
|
@ -149,6 +150,7 @@ The container name comes from Coolify's per-service container name (visible on t
|
||||||
### Confirming a tick ran
|
### Confirming a tick ran
|
||||||
|
|
||||||
Each tick:
|
Each tick:
|
||||||
|
|
||||||
- Updates `Shipments` rows in DB (status, fuel, arrival).
|
- Updates `Shipments` rows in DB (status, fuel, arrival).
|
||||||
- `POST`s to `/api/game-tick/notify` (internal SSE bus push → clients `router.refresh()`). If no connected clients, this just 204s.
|
- `POST`s to `/api/game-tick/notify` (internal SSE bus push → clients `router.refresh()`). If no connected clients, this just 204s.
|
||||||
- Emits `GameEventLog` entries you'll see in the Payload admin → Game Event Logs.
|
- Emits `GameEventLog` entries you'll see in the Payload admin → Game Event Logs.
|
||||||
|
|
@ -174,12 +176,25 @@ For dev (where `push: false` in `payload.config.ts`), run `bun run payload migra
|
||||||
Payload's `Media` collection (`src/collections/Media.ts`) uses `upload: true` with no storage adapter, so files land on local disk under `<projectRoot>/media` (i.e. `/app/media` inside the container, since `WORKDIR /app` and `process.cwd() = /app`).
|
Payload's `Media` collection (`src/collections/Media.ts`) uses `upload: true` with no storage adapter, so files land on local disk under `<projectRoot>/media` (i.e. `/app/media` inside the container, since `WORKDIR /app` and `process.cwd() = /app`).
|
||||||
|
|
||||||
In Coolify:
|
In Coolify:
|
||||||
|
|
||||||
1. On the app service → **Persistent Storage → Add Path**.
|
1. On the app service → **Persistent Storage → Add Path**.
|
||||||
2. Host path or named volume → mapped to `/app/media` in the container.
|
2. Host path or named volume → mapped to `/app/media` in the container.
|
||||||
3. Coolify preserves this volume across deploys, **including rollbacks**.
|
3. Coolify preserves this volume across deploys, **including rollbacks**.
|
||||||
|
|
||||||
Without this, every redeploy wipes user uploads.
|
Without this, every redeploy wipes user uploads.
|
||||||
|
|
||||||
|
### Map tiles volume (`/app/public/tiles`)
|
||||||
|
|
||||||
|
Map basemaps in `tiles` mode serve XYZ pyramids from `public/tiles/<map-slug>/{z}/{x}/{y}.jpg`.
|
||||||
|
The `public/tiles` directory is **gitignored** (like `media/`), so production images do not contain tiles; mount a
|
||||||
|
**Persistent Storage** volume at `/app/public/tiles` and get the tiles there by either:
|
||||||
|
|
||||||
|
- copying them into the volume's host path on the server (host-path mount: `scp -r public/tiles/antarctica/* <host-path>/`), or
|
||||||
|
- `docker cp` into the running app container before the volume is in place, or from the host into the volume.
|
||||||
|
|
||||||
|
Files added to the volume are served immediately; no restart needed. Keep the pyramid's top zoom level at the source
|
||||||
|
texture's native detail (256 x 2^z px >= source pixels) and set the map's `maxZoom` to that level.
|
||||||
|
|
||||||
> Note on the local-disk default: this works fine for a self-hosted single-instance deployment, but it does **not** scale beyond one replica (same caveat as the SSE bus). If you outgrow the single-instance architecture (more replicas, multi-host), switch to an S3-compatible storage adapter — add it to `payload.config.ts` under `plugins` (the project already has a `// storage-adapter-placeholder` hook for this) and drop the `/app/media` volume.
|
> Note on the local-disk default: this works fine for a self-hosted single-instance deployment, but it does **not** scale beyond one replica (same caveat as the SSE bus). If you outgrow the single-instance architecture (more replicas, multi-host), switch to an S3-compatible storage adapter — add it to `payload.config.ts` under `plugins` (the project already has a `// storage-adapter-placeholder` hook for this) and drop the `/app/media` volume.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -193,7 +208,13 @@ Without this, every redeploy wipes user uploads.
|
||||||
Response shape:
|
Response shape:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "ok": true, "uptime": 1243, "version": "0.1.6", "commit": "a1b2c3d", "buildTime": "2026-08-12T20:30:00.000Z" }
|
{
|
||||||
|
"ok": true,
|
||||||
|
"uptime": 1243,
|
||||||
|
"version": "0.1.6",
|
||||||
|
"commit": "a1b2c3d",
|
||||||
|
"buildTime": "2026-08-12T20:30:00.000Z"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -210,7 +231,7 @@ Recommended flow:
|
||||||
|
|
||||||
> **Tags only from `main` deploys.** Create and push the `vX.Y.Z` release tag only when deploying from `main`. Never bump versions or push release tags from feature branches: parallel branches bumping the same semver line desync `package.json` from the tags that actually deploy, and at merge time the version history no longer matches what shipped.
|
> **Tags only from `main` deploys.** Create and push the `vX.Y.Z` release tag only when deploying from `main`. Never bump versions or push release tags from feature branches: parallel branches bumping the same semver line desync `package.json` from the tags that actually deploy, and at merge time the version history no longer matches what shipped.
|
||||||
|
|
||||||
> **Version metadata in builds.** The image build embeds git info into `src/generated/versionInfo.json` (shown in the admin VersionOverlay and on `/api/version`). Coolify **deletes `.git`** from the build context, so the build reads the commit SHA from the `SOURCE_COMMIT` build arg instead of `git` (see the Dockerfile's `ARG SOURCE_COMMIT` block). For that to work, enable **"Include Source Commit in Build"** under the application's *Advanced* settings (and leave "Inject Build Args to Dockerfile" on, which is the default) — otherwise the version overlay falls back to `version` only.
|
> **Version metadata in builds.** The image build embeds git info into `src/generated/versionInfo.json` (shown in the admin VersionOverlay and on `/api/version`). Coolify **deletes `.git`** from the build context, so the build reads the commit SHA from the `SOURCE_COMMIT` build arg instead of `git` (see the Dockerfile's `ARG SOURCE_COMMIT` block). For that to work, enable **"Include Source Commit in Build"** under the application's _Advanced_ settings (and leave "Inject Build Args to Dockerfile" on, which is the default) — otherwise the version overlay falls back to `version` only.
|
||||||
>
|
>
|
||||||
> **Commit metadata availability.** On Coolify builds, `SOURCE_COMMIT` is the commit SHA, but tags, dates, and subjects are not injected automatically. If the build environment provides `GIT_COMMIT_DATE` and `GIT_COMMIT_SUBJECT`, the generator records them as `lastUpdated` and `commitMessage`; otherwise `lastUpdated` falls back to the image build time and the commit message remains unavailable. The existing `canSeeCommit` permission still gates commit details in the overlay.
|
> **Commit metadata availability.** On Coolify builds, `SOURCE_COMMIT` is the commit SHA, but tags, dates, and subjects are not injected automatically. If the build environment provides `GIT_COMMIT_DATE` and `GIT_COMMIT_SUBJECT`, the generator records them as `lastUpdated` and `commitMessage`; otherwise `lastUpdated` falls back to the image build time and the commit message remains unavailable. The existing `canSeeCommit` permission still gates commit details in the overlay.
|
||||||
>
|
>
|
||||||
|
|
@ -229,7 +250,8 @@ Coolify supports two rollout modes:
|
||||||
1. **Automatic rollback on failed health**: if the new container never becomes healthy (3 failed `/api/health` probes within the `start_period`), Coolify keeps the old container running and doesn't route traffic to the new one. Brings you back to the previous working version with no action.
|
1. **Automatic rollback on failed health**: if the new container never becomes healthy (3 failed `/api/health` probes within the `start_period`), Coolify keeps the old container running and doesn't route traffic to the new one. Brings you back to the previous working version with no action.
|
||||||
2. **Manual rollback**: in the service dashboard,/files history → pick a previous image tag → "Rollback". This redeploys that exact image with the existing `/app/media` volume intact (because the volume is mounted, not tied to the image).
|
2. **Manual rollback**: in the service dashboard,/files history → pick a previous image tag → "Rollback". This redeploys that exact image with the existing `/app/media` volume intact (because the volume is mounted, not tied to the image).
|
||||||
|
|
||||||
Rollbacks do **not** roll the database schema. If a deploy includes a non-reversible migration that has already run, rolling back the *image* but keeping the DB can break the older image's assumptions. Mitigations:
|
Rollbacks do **not** roll the database schema. If a deploy includes a non-reversible migration that has already run, rolling back the _image_ but keeping the DB can break the older image's assumptions. Mitigations:
|
||||||
|
|
||||||
- Only ship additive migrations (don't drop columns in the same deploy that removes their consumer code — drop them in a follow-up deploy instead).
|
- Only ship additive migrations (don't drop columns in the same deploy that removes their consumer code — drop them in a follow-up deploy instead).
|
||||||
- For destructive changes, take a Coolify DB snapshot first.
|
- For destructive changes, take a Coolify DB snapshot first.
|
||||||
|
|
||||||
|
|
@ -269,15 +291,15 @@ docker compose exec app sh # shell inside the app container
|
||||||
|
|
||||||
## 13. Quick troubleshooting
|
## 13. Quick troubleshooting
|
||||||
|
|
||||||
| Symptom | Likely cause | Fix |
|
| Symptom | Likely cause | Fix |
|
||||||
|---|---|---|
|
| ------------------------------------------------------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| Container restart loops, never healthy | DB connection unreachable | Verify `DATABASE_URI` resolves from inside the container (`docker compose exec app wget -qO- $DATABASE_URI:5432` or `nc -zv ...`). Coolify: confirm the Postgres and app services are in the same Project so Coolify's network reaches it. |
|
| Container restart loops, never healthy | DB connection unreachable | Verify `DATABASE_URI` resolves from inside the container (`docker compose exec app wget -qO- $DATABASE_URI:5432` or `nc -zv ...`). Coolify: confirm the Postgres and app services are in the same Project so Coolify's network reaches it. |
|
||||||
| 401 loop after deploy | `PAYLOAD_SECRET` changed without rotating user sessions | Keep `PAYLOAD_SECRET` constant across deploys of the same env. If you must rotate, users will need to re-log in (cookies are JWT-signed against this secret). |
|
| 401 loop after deploy | `PAYLOAD_SECRET` changed without rotating user sessions | Keep `PAYLOAD_SECRET` constant across deploys of the same env. If you must rotate, users will need to re-log in (cookies are JWT-signed against this secret). |
|
||||||
| Realtime updates stopped working | More than 1 replica running, OR scheduled tick stopped | Limit replicas to 1. Check the scheduled tasks in Coolify are firing and `GameEventLogs` show fresh `gameTick` events. |
|
| Realtime updates stopped working | More than 1 replica running, OR scheduled tick stopped | Limit replicas to 1. Check the scheduled tasks in Coolify are firing and `GameEventLogs` show fresh `gameTick` events. |
|
||||||
| First build OOMs | Host has < 8 GB RAM during Next build | The `bun run build` script sets `--max-old-space-size=8000`. Give the Coolify build host at least 8 GB, or lower this in `package.json#scripts.build` (memory will become the limiter earlier). |
|
| First build OOMs | Host has < 8 GB RAM during Next build | The `bun run build` script sets `--max-old-space-size=8000`. Give the Coolify build host at least 8 GB, or lower this in `package.json#scripts.build` (memory will become the limiter earlier). |
|
||||||
| `bun run db push` fails PostGIS error | This is expected (`must be owner of table spatial_ref_sys`) | Use `bun run payload migrate` only. See `AGENTS.md` Gotchas. |
|
| `bun run db push` fails PostGIS error | This is expected (`must be owner of table spatial_ref_sys`) | Use `bun run payload migrate` only. See `AGENTS.md` Gotchas. |
|
||||||
| Token set but bot never logs in / silent bot death | Entrypoint gate, config throw, or revoked/invalid token | Check the container logs for the bot's fail-fast line. Confirm `DISCORD_GUILD_ID` is set (the config throws at import if missing) and the token is valid/not revoked. The web app is unaffected — the bot is an in-process subordinate. |
|
| Token set but bot never logs in / silent bot death | Entrypoint gate, config throw, or revoked/invalid token | Check the container logs for the bot's fail-fast line. Confirm `DISCORD_GUILD_ID` is set (the config throws at import if missing) and the token is valid/not revoked. The web app is unaffected — the bot is an in-process subordinate. |
|
||||||
| Scheduled task / bot dies with `Illegal instruction (core dumped)` | Running Bun on the host CPU (no AVX/AVX2) | The runtime image is Node-only — never `docker exec ... bun ...`. Bins: `node node_modules/payload/bin.js <cmd>`; bot: `node --import tsx src/bot/index.ts`. |
|
| Scheduled task / bot dies with `Illegal instruction (core dumped)` | Running Bun on the host CPU (no AVX/AVX2) | The runtime image is Node-only — never `docker exec ... bun ...`. Bins: `node node_modules/payload/bin.js <cmd>`; bot: `node --import tsx src/bot/index.ts`. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ ENV NODE_ENV=production \
|
||||||
HOSTNAME=0.0.0.0
|
HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
# The `node` user already exists in `node:*-alpine` images.
|
# The `node` user already exists in `node:*-alpine` images.
|
||||||
RUN mkdir -p /app/media \
|
RUN mkdir -p /app/media /app/public/tiles \
|
||||||
&& chown -R node:node /app
|
&& chown -R node:node /app
|
||||||
|
|
||||||
# 1) Next.js standalone runtime (server.js + traced node_modules + .next).
|
# 1) Next.js standalone runtime (server.js + traced node_modules + .next).
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
# Persist Payload uploads between rebuilds locally.
|
# Persist Payload uploads between rebuilds locally.
|
||||||
- ./media:/app/media
|
- ./media:/app/media
|
||||||
|
# Map tile pyramids (gitignored; generated locally, uploaded in prod).
|
||||||
|
- ./public/tiles:/app/public/tiles
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -63,4 +65,4 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue