18 KiB
Install a clone and choose an update
Requirements and first installation
Use a Linux host with Docker Engine, the Compose plugin, Git and flock. This Compose file uses host networking and port 3002. Provide an existing compatible Habbo MariaDB database, reachable Redis, persistent storage and an HTTP(S) reverse proxy. The installer does not provision the emulator, a database, TLS, or a registry account.
Clone this repository from the Gitea URL supplied by your administrator, enter the clone, then run:
bash cms install
The wizard asks for the public URL and delivery mode. Source (the default for a new installation) builds the locked source locally and only needs repository access plus access to public build dependencies. Prebuilt downloads the application and migrations from the repository's docker-image.txt; a private package needs a separate registry login with package-read permission. Git access alone may not grant package access. Existing saved mode and .env are preserved. bash cms install --configure-only saves configuration without starting containers.
Credentials are entered on the host, never in the dashboard. Do not paste .env into support tickets. Installation prepares public/nitro-assets, public/swf, storage, and /var/www/Gamedata for UID/GID 33 without recursively taking ownership of existing files. Existing nested assets may still need operator permission repair. The updater tests access to those mounts as the candidate container user before migrations; this checks directory access, not every nested file or filesystem capacity.
After startup, visit /admin/devops/installation with the appropriate permission. Verify database, Redis, storage and migration status. Worker heartbeat is a separate runtime signal: a healthy HTTP endpoint does not prove an import worker is processing jobs. Check the worker status and investigate a missing/stale heartbeat before scheduling imports. The dashboard is read-only and cannot start Docker, upgrade the host or grant registry access.
Client IP trust at the reverse proxy
The application validates and normalizes client addresses from cf-connecting-ip, the first x-forwarded-for entry, then x-real-ip. It never accepts x-real-client-ip; that legacy derived header is also stripped by the Next.js proxy. Missing or invalid addresses resolve to 0.0.0.0 for rate limits and audit records. API routes use the same resolver even though they do not run through the Next.js proxy.
These headers are trustworthy only when the ingress sanitizes them. Configure the reverse proxy to discard client-supplied forwarding/derived headers and replace the accepted address from a verified connection or a specifically trusted upstream proxy. Do not append an untrusted incoming x-forwarded-for chain and then treat its first entry as authoritative. Forward cf-connecting-ip only after verifying that it came through your trusted CDN path; otherwise remove it.
Restrict direct access to the application port so requests must pass through that ingress. The provided Compose file uses host networking with HOSTNAME=0.0.0.0; it does not enforce this restriction or provision nginx/Traefik trust rules. Verify the host firewall and actual reverse-proxy configuration before relying on client IPs for blocking, auditing or abuse limits. Repository tests prove rejection of the derived-header bypass and malformed addresses; they do not certify the deployed forwarding trust chain.
Opt-in profile: Nginx on the same host
Use this profile for a new Linux Compose clone whose public HTTPS endpoint is Nginx on that same host. It leaves docker-compose.yml and CI-managed production deployments unchanged. Nginx must include ngx_http_realip_module; check nginx -V before using the templates. The CMS remains on the existing host network for database/Redis connectivity, but its HTTP process listens on 127.0.0.1:3002. Host networking shares the host network namespace; a ports: mapping would not provide the restriction. See Docker host networking.
-
Run
bash cms install --configure-onlyand choose the intended public HTTPS URL. Obtain a valid certificate for that hostname using the host's existing certificate-management process. The templates do not issue certificates or configure renewal. -
In the clone's existing
.env, add or replace this single setting, preserving all other values:COMPOSE_FILE=docker-compose.yml:deployment/proxy/compose.loopback.ymlKeep
APP_URL,AUTH_URLand the saved installer public URL on the same canonicalhttps://hostname. The profile pins the existing port 3002 as well as the loopback address. Do not placeCOMPOSE_FILEin.docker-install; that file accepts onlyMODEandPUBLIC_URL. -
Copy nginx-direct.example.conf into the host's Nginx configuration directory, outside this Git clone. Replace every
hotel.exampleand both certificate paths. Load it athttpscope, for example through/etc/nginx/conf.d/cms.conf. Review any existing virtual host for that hostname to avoid two competing configurations. The example handles only CMS HTTP traffic; emulator WebSocket and other hotel services need their own reviewed ingress. -
Validate the effective Nginx configuration with
nginx -t, then enable/reload it through the host's normal service-management procedure. Prepare this endpoint before starting the installer, because installation verifies the saved public URL. It can return an upstream-unavailable response until the CMS starts. -
From the clone root, remove conflicting Compose overrides from the deployment shell and validate the model:
unset COMPOSE_FILE COMPOSE_PATH_SEPARATOR COMPOSE_ENV_FILES COMPOSE_DISABLE_ENV_FILE docker compose config --quiet bash cms installThese
unsetcommands remove shell overrides; they do not remove theCOMPOSE_FILEline in.env. Do not setCOMPOSE_DISABLE_ENV_FILE=1, use an alternate--env-file, or supply a competing shellCOMPOSE_FILEfor this workflow. Environment values can override.envselection. Do not print or paste the full rendered Compose configuration, because it contains runtime credentials. See Compose predefined variables and precedence. -
Confirm the running configuration without dumping the environment:
docker compose exec -T cms node -e 'if(process.env.HOSTNAME!=="127.0.0.1"||process.env.PORT!=="3002")process.exit(1);console.log("CMS configured for 127.0.0.1:3002")' ss -lnt '( sport = :3002 )' curl --fail --silent --show-error https://hotel.example/api/healthThe listener must be
127.0.0.1:3002, not0.0.0.0:3002or[::]:3002. From another machine, the host's public IP on port 3002 must be unreachable. Check both address families when the host has IPv6. Loopback isolation covers the CMS process only: other containers and host services, including Byparr, retain their existing bindings.
The direct template uses the original socket peer ($realip_remote_addr), overwrites the two accepted forwarding headers, and removes incoming CF-Connecting-IP, X-Real-Client-IP and Forwarded. It fixes forwarded host/protocol to the configured HTTPS origin. An unrelated inherited real-IP rule cannot turn a caller-supplied header into the forwarded client address in this mode. See Nginx original-peer variables and header replacement/removal.
Response buffering is disabled for progress streams, and this example adds no proxy response cache or CORS policy. Its 64 MiB ingress body cap accommodates the existing 52 MiB Studio attachment limit; route and Server Action limits remain authoritative and may be lower. TLS 1.2/1.3 are configured explicitly. See Nginx buffering and TLS protocols.
Why the profile survives an update
The installer preserves an existing .env. The installer invokes docker-update.sh, and the updater's config, build, candidate run, cutover and rollback paths all call bare docker compose from the clone root, without -f. Compose therefore reads the saved file list on each invocation. The base file appears first so its relative mount/build paths remain rooted in the clone; the later profile overrides only the CMS environment and healthcheck. No installer/updater patch is required for this selection. See Compose merge order and relative paths.
Keep the profile selected for every routine bash cms update and selected-release update. A one-off docker compose -f ... up does not persist selection for later installer/updater commands. Do not add an untracked docker-compose.override.yml: it makes the clone dirty unless separately excluded and is bypassed when COMPOSE_FILE selects an explicit list. Before selecting an older release, verify that deployment/proxy/compose.loopback.yml exists in that release; a missing selected file fails configuration rather than silently using the public bind.
This profile does not change scripts/ci-deploy.sh, which owns a separate docker run deployment and currently sets its own public binding. Do not use the clone installer to take over a host managed by CI. Restricting that deployment's listener requires a separate compatibility review and rollout.
Separate mode: verified remote edge or Cloudflare
A remote proxy cannot connect to the CMS loopback listener directly. The supported topology here is remote edge → TLS → Nginx on the CMS host → loopback CMS, retaining the same Compose profile. Use nginx-trusted-proxy.example.conf instead of the direct template. Do not enable both templates for one hostname.
For your own remote edge, replace 203.0.113.10/32 in both the geo peer allowlist and set_real_ip_from with the exact approved connection source addresses. The reserved example address deliberately permits no real edge. Require the edge to overwrite X-Forwarded-For with one verified client IP, use the configured hostname, enforce public HTTPS, and validate this origin's TLS certificate. The origin checks the original socket peer before accepting the rewritten address. Never use 0.0.0.0/0, ::/0 or arbitrary client networks as trusted proxies. See Nginx real-IP trust configuration.
For Cloudflare, use that same restricted-edge mode and replace both peer lists with the current verified IPv4 and IPv6 Cloudflare ranges, maintained by the operator; use real_ip_header CF-Connecting-IP instead of X-Forwarded-For. Configure Full (strict) TLS and review authenticated origin pulls. Obtain ranges from Cloudflare's official IP list and follow its visitor-IP restoration guidance. Do not copy a historical range list from a support ticket or trust CF-Connecting-IP merely because it is present. This setup still removes the CF header before the CMS and forwards only Nginx's normalized result in XFF/X-Real-IP.
The direct template intentionally records the CDN/edge socket address when placed behind an unconfigured CDN; it does not silently trust an upstream header. After configuring the restricted-edge mode, verify with requests from an allowed edge and a disallowed direct client, including forged CF/XFF headers, and check the recorded client address. Neither the repository nor the installer changes the host firewall or certifies another proxy's header behavior.
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 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.
Opt-in: CrowdSec on the same Docker host
A self-contained CrowdSec engine ships in deployment/crowdsec. It reads the host Nginx access log, runs crowdsecurity/crowdsec:1.8.1 in its own Compose project and exposes LAPI only on 127.0.0.1:18080. No reverse-proxy, Traefik, Cloudflare or firewall configuration is changed.
bash cms security
The command generates CROWDSEC_LAPI_API_KEY, writes the CrowdSec flags into .env, starts the engine and registers the cms bouncer. The anti-DDoS gate then consults the local LAPI per client IP (short-cached) and blocks ban/captcha decisions before its own rate buckets. bash cms security status reports engine state and bash cms security disable stops the engine and flips the toggle off.
The engine does not enroll into the CrowdSec Central API (DISABLE_ONLINE_API=true): detection stays local. The app still has its separate opt-in traffic-sharing channel via CROWDSEC_REPORT_ENABLED. Change CROWDSEC_LAPI_PORT and CROWDSEC_LAPI_URL together when 18080 is already in use. CROWDSEC_NGINX_LOG_DIR overrides the log directory the engine acquires.
This bouncer is application-layer: it sheds known-bad IPs at the CMS process and only for traffic that reaches the Next.js proxy. It does not drop traffic before the origin, does not protect other host ports/services, and depends on the client IP being trustworthy at the ingress. Keep the upstream protections (Cloudflare IP rules, proxy rate limits) for defense before the origin.
The running CMS loads the new env values on its next restart or deployment. For a CI-managed epicnext-cms-app, the next deploy (which sources .env) applies them; for a clone, bash cms update --skip-pull restarts it. .env now holds the LAPI key — keep its permissions restrictive.
External IP blocklists (Spamhaus, DShield, CINS, blocklist.de, abuse.ch, IPsum, Firehol, Tor exit nodes, …) can be synced into the local LAPI with bash cms security blocklists, and hourly with bash cms security blocklists-install-cron (no account, but internet to fetch). Configure via CROWDSEC_BLOCKLIST_*.
Routine and selected-release updates
bash cms update
This requires a clean clone and fast-forwards its configured Git upstream, then builds/pulls artifacts for that exact commit. It validates the application and migration revision labels, validates runtime configuration and storage, runs migrations and verifies the recreated CMS locally and through the saved public URL.
To install a specific published version, fetch it and select its commit on the host first:
git fetch origin
git switch --detach <reviewed-commit-or-tag>
bash cms update --skip-pull
--skip-pull deliberately uses the checked-out commit, including detached HEAD. For routine updates again, switch back to your tracked deployment branch. The script does not invent version-to-schema compatibility or automatically change branches.
In prebuilt mode you can additionally require immutable artifacts from the configured repository:
bash cms update --skip-pull --app-digest sha256:<64-lowercase-hex> --migrations-digest sha256:<64-lowercase-hex>
Replace both placeholders with publisher-provided digests. Both are mandatory together. Revision labels must match the checked-out commit; a digest alone does not establish schema compatibility. Older migration images without a revision label must be republished from matching source, or the same checkout can be installed in source mode. No migration runs when the artifact or candidate validation fails.
Compatibility and recovery
Before upgrading, read the selected release's migration changes and take a database backup with a tested restore procedure. Preserve .env, persistent assets and the previous release identifier. The current tooling does not provide a validated matrix of supported source/target database versions. Pinning an old commit is therefore not a supported database downgrade procedure.
On a failure after container replacement, the updater attempts to restore the previous image and checks it locally. Image rollback does not reverse database migrations. A migration may partially apply or make the old application incompatible, including when migration fails before container replacement. Recover the database only through the reviewed backup/restore procedure and coordinate downtime; do not assume restarting the old image recovers it. First installation has no prior image to restore. Host logs remain in logs/docker-update.log and may contain application/database diagnostics; restrict access.
Local Git Bash tests cover selection validation and mocked failure paths. They do not establish Linux container startup, runtime filesystem permissions, registry availability or real MariaDB upgrade compatibility.