fix(compose): keep the CMS opt-in so it cannot claim the CI port
CI / check (push) Successful in 36s
CI / tests-imaging-browsers (push) Successful in 1m49s
CI / tests-integration (push) Successful in 1m59s
CI / tests-unit (push) Successful in 2m4s
CI / tests-quality (push) Successful in 42s
CI / tests-ui (push) Successful in 4m39s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m20s
CI / check (push) Successful in 36s
CI / tests-imaging-browsers (push) Successful in 1m49s
CI / tests-integration (push) Successful in 1m59s
CI / tests-unit (push) Successful in 2m4s
CI / tests-quality (push) Successful in 42s
CI / tests-ui (push) Successful in 4m39s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m20s
On a CI-managed host scripts/ci-deploy.sh owns port 3002 through the blue/green slots, so a compose replica of the cms service could only ever crash-loop on EADDRINUSE — it never serves traffic, because nginx points at a slot container. Gate the service behind the `cms` profile so a bare `docker compose up -d` starts only byparr. Naming the service explicitly (`docker compose up -d cms`, which is what the installer and updater already do) still enables the profile automatically, so the compose-only install path keeps working. Keep the one port that is actually served (3002) and make the 2 GB ceiling hard by capping swap at 2g as well. Drop the green compose service (3003): the green slot only ever exists as a docker run container created by CI, so compose no longer defines it and the two paths cannot drift apart. The loopback override contract stays covered: proxy-config.test.mjs now renders the configuration with --profile cms, and a new build-contract test pins the profile and the memory limits so neither can be dropped by accident.
This commit is contained in:
1 parent
5c428c4526
commit
597287fce5
5 files changed
+52
-22
No files matched your search
@@ -965,11 +965,13 @@ there are two fixed slots, and each release lands in the idle one:
|
||||
| A | 3002 | `epicnext-cms-app` |
|
||||
| B | 3003 | `epicnext-cms-green` |
|
||||
|
||||
`docker-compose.yml` defines both services (`cms`, and `cms-green` behind the
|
||||
`green` profile) from one shared YAML anchor so they cannot drift apart. Note
|
||||
that the CI deploy path deliberately uses `docker run` rather than compose, so
|
||||
the resource limits are declared in **both** places — limits that only existed
|
||||
in compose would never apply to a real release.
|
||||
`docker-compose.yml` defines a single `cms` service (the blue slot on 3002)
|
||||
behind the `cms` profile, so a bare `docker compose up -d` never starts it. The
|
||||
green slot (`epicnext-cms-green`, port 3003) only ever exists as a `docker run`
|
||||
container created by the CI deploy; compose does not define it, so the two paths
|
||||
cannot drift apart. Note that the CI deploy path deliberately uses `docker run`
|
||||
rather than compose, so the resource limits are declared in **both** places —
|
||||
limits that only existed in compose would never apply to a real release.
|
||||
|
||||
### Compose on a CI host
|
||||
|
||||
@@ -977,9 +979,14 @@ Compose and CI both want port 3002, so only one of them can own a host. A stray
|
||||
compose replica (`docker compose up`, or the daily `scripts/docker-update.sh`
|
||||
cron) parked an `epicnext-cms` container on the blue slot while nginx served the
|
||||
green slot, and every later release stopped on "Port 3002 is already in use" —
|
||||
after the build, the migrations and the browser gate. Two guards now prevent
|
||||
after the build, the migrations and the browser gate. Three guards now prevent
|
||||
that:
|
||||
|
||||
- The `cms` service sits behind the `cms` profile, so a bare `docker compose up
|
||||
-d` starts only byparr and can never claim 3002. The service starts only when
|
||||
it is named explicitly (`docker compose up -d cms`), which is what the
|
||||
installer/updater already do; Compose enables the profile automatically for an
|
||||
explicitly named service.
|
||||
- `scripts/docker-update.sh` refuses to run on a CI host. It used to test only
|
||||
`epicnext-cms-app`, but after a cutover to the green slot that container is
|
||||
stopped and deleted, so the guard stopped firing while the host stayed
|
||||
|
||||
+22
-13
@@ -1,5 +1,20 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Next.js CMS — blue/green
|
||||
# Next.js CMS — single slot on port 3002
|
||||
#
|
||||
# ── Eigenaar van poort 3002 ─────────────────────────────────────────────────
|
||||
# Deze repo kent twee deploy-paden die allebei op poort 3002 willen:
|
||||
# * `scripts/ci-deploy.sh` (blue/green, docker run) — de eigenaar op een
|
||||
# CI-host. Elke push naar main bouwt daar een nieuwe image, draait de
|
||||
# migrations en zet nginx pas om na health + e2e + imaging.
|
||||
# * deze compose-`cms`-service — bedoeld voor een LOSSE host zonder CI.
|
||||
#
|
||||
# Ze kunnen niet naast elkaar draaien: wie op 3002 zit, blokkeert de ander met
|
||||
# `EADDRINUSE`. Daarom staat `cms` achter een profile. Een bloot
|
||||
# `docker compose up -d` start dus alleen byparr en kan nooit de live CI-release
|
||||
# van de site drukken. De service start alleen als je hem expliciet noemt
|
||||
# (`docker compose up -d cms`, `docker compose build cms`, ...): Compose schakelt
|
||||
# het profile van een expliciet genoemde service vanzelf in. Op een CI-host wil
|
||||
# je dat dus juist NOOIT doen.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
x-cms: &cms
|
||||
image: epicnext-cms:${CMS_RELEASE:-local}
|
||||
@@ -14,8 +29,8 @@ x-cms: &cms
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
NEXT_DEPLOYMENT_ID: ${CMS_RELEASE:-unknown}
|
||||
# Runtime host networking IS required: blue/green needs per-release host ports
|
||||
# (3002/3003) and nginx reaches the slot over 127.0.0.1.
|
||||
# Runtime host networking IS required: nginx reaches the CMS over 127.0.0.1
|
||||
# on the single port that is actually served (3002).
|
||||
network_mode: host
|
||||
stop_grace_period: 15s
|
||||
restart: unless-stopped
|
||||
@@ -28,8 +43,8 @@ x-cms: &cms
|
||||
- /var/www/Gamedata:/var/www/Gamedata
|
||||
|
||||
# ── Resource limits ──
|
||||
mem_limit: 6g
|
||||
memswap_limit: 7g
|
||||
mem_limit: 2g
|
||||
memswap_limit: 2g
|
||||
cpus: 2.0
|
||||
pids_limit: 512
|
||||
|
||||
@@ -43,19 +58,13 @@ x-cms: &cms
|
||||
services:
|
||||
cms:
|
||||
<<: *cms
|
||||
# Alleen starten als je deze host bewust compose-only beheert.
|
||||
profiles: ["cms"]
|
||||
container_name: epicnext-cms
|
||||
environment:
|
||||
- HOSTNAME=0.0.0.0
|
||||
- PORT=3002
|
||||
|
||||
cms-green:
|
||||
<<: *cms
|
||||
container_name: epicnext-cms-green
|
||||
profiles: ["green"]
|
||||
environment:
|
||||
- HOSTNAME=0.0.0.0
|
||||
- PORT=3003
|
||||
|
||||
byparr:
|
||||
image: ghcr.io/thephaseless/byparr:latest
|
||||
container_name: byparr
|
||||
|
||||
@@ -81,7 +81,7 @@ The direct template intentionally records the CDN/edge socket address when place
|
||||
|
||||
### Local verification and deployment boundary
|
||||
|
||||
`pnpm exec vitest run --coverage.enabled=false scripts/proxy-config.test.mjs` runs the installed Docker Compose CLI against a disposable clone configuration, without contacting Docker Engine, building images or reading the real `.env`. It verifies selection through `.env`, loopback/port override precedence, the IPv4 healthcheck and preservation of mounts, release selection and host networking. It explicitly skips when Compose is unavailable. This is not a container-start or network-isolation test.
|
||||
`pnpm exec vitest run --coverage.enabled=false scripts/proxy-config.test.mjs` runs the installed Docker Compose CLI against a disposable clone configuration, without contacting Docker Engine, building images or reading the real `.env`. It verifies selection through `.env`, loopback/port override precedence, the IPv4 healthcheck and preservation of mounts, release selection and host networking. The `cms` service is behind the `cms` profile (so a bare `docker compose up -d` cannot claim port 3002 on a CI-managed host), so the test renders the configuration with `--profile cms`. It explicitly skips when Compose is unavailable. This is not a container-start or network-isolation test.
|
||||
|
||||
`pnpm test:integration` additionally starts disposable Nginx containers from the actual templates, supplies a temporary test certificate, and sends real HTTPS requests with forged identity headers. It checks direct-mode replacement even with an inherited real-IP rule, rejection of untrusted peers, and acceptance through an explicitly trusted peer. This requires Docker Engine and the OpenSSL CLI and does not read deployment credentials. The templates must still pass `nginx -t` on the intended host after its hostname/certificate substitution, then the listener and trusted-header checks above; the disposable fixture cannot certify that host or its firewall.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const hasCompose =
|
||||
}).status === 0;
|
||||
|
||||
it.skipIf(!hasCompose)(
|
||||
"loads the loopback profile from .env for bare Compose commands without changing mounts or host networking",
|
||||
"loads the loopback override from .env for the cms service without changing mounts or host networking",
|
||||
() => {
|
||||
const directory = mkdtempSync(path.join(tmpdir(), "cms-proxy-config-"));
|
||||
try {
|
||||
@@ -50,9 +50,12 @@ it.skipIf(!hasCompose)(
|
||||
["CMS_RELEASE", "HOSTNAME", "PORT"].includes(key)
|
||||
)
|
||||
delete environment[key];
|
||||
// `cms` is behind the `cms` profile so a bare `docker compose up -d`
|
||||
// can never squat port 3002 on a CI-managed host. Selecting the
|
||||
// profile here keeps the loopback/port-override contract under test.
|
||||
const result = spawnSync(
|
||||
"docker",
|
||||
["compose", "config", "--format", "json"],
|
||||
["compose", "--profile", "cms", "config", "--format", "json"],
|
||||
{ cwd: directory, env: environment, encoding: "utf8", timeout: 15_000 },
|
||||
);
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
|
||||
@@ -47,6 +47,17 @@ it("passes a compiled release to both the application build and final image", ()
|
||||
expect(compose).toContain("NEXT_DEPLOYMENT_ID: ${CMS_RELEASE:-unknown}");
|
||||
});
|
||||
|
||||
it("keeps the compose CMS opt-in and inside the 2 GB limit", () => {
|
||||
const compose = readFileSync("docker-compose.yml", "utf8");
|
||||
// A bare `docker compose up -d` starts only byparr. On a CI-managed host the
|
||||
// CMS owns port 3002 through ci-deploy.sh; letting compose claim the same
|
||||
// port again produces an endless EADDRINUSE crash loop.
|
||||
expect(compose).toContain('profiles: ["cms"]');
|
||||
expect(compose).not.toContain("cms-green");
|
||||
expect(compose).toContain("mem_limit: 2g");
|
||||
expect(compose).toContain("memswap_limit: 2g");
|
||||
});
|
||||
|
||||
it("builds with fixtures and excludes installation secrets from every stage", () => {
|
||||
const ignored = readFileSync(".dockerignore", "utf8");
|
||||
expect(ignored).toMatch(/^\.env$/m);
|
||||
|
||||
Reference in new issue
Block a user