fix(deploy): verify Docker clone updates against the served release
CI / check (push) Successful in 59s
CI / deploy (push) Failing after 1m21s

This commit is contained in:
Simo committed 2026-09-07 21:17:34 +02:00
1 parent 3bac126ace
commit cbaa115d56
15 files changed
+321 -187

No files matched your search

+67 -56
View File
@@ -137,79 +137,89 @@ pm2 stop next 2>/dev/null || true
# 3. Ensure the write directories are owned by www-data (UID/GID 33) so the
# container user can write imports/uploads to them.
sudo chown -R 33:33 ./public/nitro-assets ./public/swf ./storage
sudo mkdir -p ./public/nitro-assets ./public/swf ./storage /var/www/Gamedata
sudo chown -R 33:33 ./public/nitro-assets ./public/swf ./storage /var/www/Gamedata
# 4. Build and start
docker compose up -d --build
# 4. Build, migrate and start a release tied to the Git commit.
# Requires Linux, Bash, Git, flock and Docker Compose v2; no host Node/pnpm.
bash scripts/docker-update.sh
# 5. Run migrations. The slim runtime image has no source/tsx, so run migrations
# on the HOST (against the same database) before/after starting the container.
pnpm db:migrate # or: npm run db:migrate / yarn db:migrate
# 6. Check logs
# 5. Follow logs
docker compose logs -f cms
```
The CMS will be available at `http://localhost:3002`.
The CMS listens on port 3002 using Linux host networking. Existing database,
Redis, emulator and shared gamedata services must already be configured.
Create the mounted directories before installation; do not mount the checkout,
`.next` or `node_modules` over `/app` inside the production container.
> **Reverse proxy:** This setup is designed to run behind the existing nginx on the host. nginx serves the Nitro/Octane client (`/client/`, `/nitro-client/`) and `/gamedata/` directly from `/var/www/Octane/dist` and `/var/www/Gamedata`, and proxies `/` to the CMS on `127.0.0.1:3002`. Point `APP_URL`/`NEXT_PUBLIC_APP_URL` at the public site URL.
### Updating a Docker clone
### Automatic Updates
Updating is fully scripted — no need to touch Docker or nginx by hand. The
script `scripts/docker-update.sh` pulls the latest `main`, runs CMS migrations
on the host, rebuilds the image, recreates the container and waits for a healthy
status. It also makes sure a stale host-side PM2 CMS (`pm2 stop next`) stays
stopped so it can't clash on port 3002.
**Automatically (recommended):** a nightly cron job is already configured on a
production server that installed this setup. It runs the script every night at
03:30 and appends to `logs/docker-update.cron.log`:
From your clone, run:
```bash
crontab -e
# 30 3 * * * /var/www/atom-nexst/scripts/docker-update.sh >> /var/www/atom-nexst/logs/docker-update.cron.log 2>&1
# Also verifies that your public domain serves the expected commit:
CMS_PUBLIC_URL=https://your-hotel.example bash scripts/docker-update.sh
```
**Manually** — to update right now (same steps as the cron runs):
The script follows the **current branch's configured Git upstream**. It refuses
local uncommitted/untracked work, pulls fast-forward only and restarts itself if
the updater changed. It does not reset or delete local work. Configure the
upstream to your fork/branch if that is where you receive updates.
It builds a commit-tagged image and runs migrations in the matching builder
container, using the clone's `.env`; no host dependency installation is needed.
Only after build and migrations succeed does it recreate the CMS service. It
compares the running image ID, image revision and `/api/health` release with the
expected Git commit. With `CMS_PUBLIC_URL`, it also checks the domain through
your proxy/CDN. A healthy response from another release is an error.
`git pull` alone updates source files, not an existing container. `docker compose
restart` restarts the same image. `docker compose pull` downloads registry images;
it does not fetch changes to this Git-built CMS. Use the updater for releases.
A clone does **not** install an automatic scheduler. If desired, explicitly add a
cron entry with the absolute path of **your** checkout; keep logs in its `logs`
directory. Do not configure both CI and Compose updates for the same instance.
The updater refuses an active `epicnext-cms-app` CI-managed container.
Logs: `logs/docker-update.log`. A failure after recreation leaves the candidate
running for diagnosis and returns nonzero; this Compose updater does not promise
automatic application or database rollback. Persistent volumes are preserved.
Before production migrations, retain your normal database backup. The existing
CI deploy continues to restore its previous container on failed verification.
### Diagnose an update that is not visible
```bash
cd /var/www/atom-nexst
./scripts/docker-update.sh
# log: logs/docker-update.log
git rev-parse HEAD
docker inspect --format '{{.Image}}' epicnext-cms
docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' epicnext-cms
curl -fsS http://127.0.0.1:3002/api/health
curl -fsS https://your-hotel.example/api/health
```
The script aborts safely (exit 1) if the working tree has uncommitted changes so
a `git pull` can never clobber local edits, and leaves the container running if
health fails so you can debug it (exit 3). Failed runs are reported in the log;
an exit of 0 means the CMS is healthy on the new commit.
Both HTTP responses must report the expected `release`. `unknown` means the image
was built without a commit ID. If the local endpoint is current but the public
one is old, check nginx's upstream, other CMS processes and proxy/CDN caching.
Health responses must not be cached. The release is compiled into Next.js and
cannot be changed simply by injecting a new variable into an old container.
### Docker Commands
### Docker commands
| Command | Description |
| ------------------------------------------ | ------------------------------------ |
| `docker compose up -d --build` | Build and start in background |
| `docker compose down` | Stop and remove containers |
| `docker compose logs -f cms` | Follow CMS logs |
| `docker compose exec cms sh` | Open a shell in the CMS container |
| `docker compose restart cms` | Restart the CMS container |
| `docker compose pull && docker compose up -d --build` | Update and redeploy |
| `pnpm db:migrate` (on the **host**) | Run database migrations (slim image has no source) |
| `./scripts/docker-update.sh` | Full automated update (manual or cron) |
| `pnpm db:up` / `pnpm db:down` | Start / stop the `mariadb-turbo` bulk-load container |
| Command | Purpose |
| --- | --- |
| `bash scripts/docker-update.sh` | Pull, build, migrate, recreate and verify |
| `docker compose logs -f cms` | View CMS logs |
| `docker compose exec cms sh` | Open a shell in the running CMS |
| `docker compose restart cms` | Restart the existing release |
| `docker compose down` | Stop services; does not update code |
| `pnpm db:up` / `pnpm db:down` | Manage the optional MariaDB service |
### How Package Manager Detection Works
The Dockerfile checks for lockfiles in this order:
1. **`pnpm-lock.yaml`** → uses pnpm (fastest, recommended)
2. **`yarn.lock`** → uses yarn
3. **`package-lock.json`** → uses npm
4. **No lockfile** → falls back to `npm install`
This means you can use any package manager on your host machine — the Docker build will automatically match.
> Override the detection explicitly with `docker compose build --build-arg PACKAGE_MANAGER=pnpm` (or `npm` / `yarn`).
For a manual build, export `CMS_RELEASE=$(git rev-parse HEAD)` before
`docker compose build cms`; deployment and migration verification remain your
responsibility. Docker uses the committed pnpm lockfile and pinned Node version.
The build cache can stay enabled: copying changed source invalidates the
application build layer. Deleting all Docker cache is not an update mechanism.
### Volumes
@@ -235,7 +245,8 @@ Only the CMS (and the optional avatar imager container, see [Avatar Imaging](#av
**Container user & write permissions:** the CMS container runs as `www-data` (UID/GID 33) by default to match the host owner of `/var/www/Gamedata`. Ensure the other write volumes (`./public/nitro-assets`, `./public/swf`, `./storage`) are also owned by `www-data` on the host:
```bash
sudo chown -R 33:33 ./public/nitro-assets ./public/swf ./storage
sudo mkdir -p ./public/nitro-assets ./public/swf ./storage /var/www/Gamedata
sudo chown -R 33:33 ./public/nitro-assets ./public/swf ./storage /var/www/Gamedata
sudo chmod -R o+rX ./public/nitro-assets ./public/swf ./storage
```