services: cms: image: epicnext-cms:${CMS_RELEASE:-local} build: context: . dockerfile: Dockerfile args: NEXT_DEPLOYMENT_ID: ${CMS_RELEASE:-unknown} # The host disables Docker iptables (daemon.json: "iptables": false), so # build containers on the bridge network have no outbound NAT/DNS. Build on # the host network instead so pnpm/npm/yarn can reach the registry. network: host container_name: epicnext-cms # Runs on the host network so existing 127.0.0.1 refs in .env keep working: # MariaDB (3306), DragonflyDB/Redis (6379), emulator RCON (3003) + API (3001), # and the imaging renderer (8082). The container then listens directly on the # host's 3002 (the same port the current host-side CMS uses). # NOTE: stop the host CMS (next-server on 3002) first, otherwise the port is taken. network_mode: host restart: unless-stopped env_file: - .env environment: - HOSTNAME=0.0.0.0 volumes: # ── Write targets (runtime imports/uploads, persistent on the host) ── # The CMS writes imported furni/figures/pets/effects here (see # src/lib/services/furni-asset-dirs.ts). These must be RW, and owned by # UID/GID 33 (www-data) on the host so the container user can write to them: # sudo chown -R 33:33 ./public/nitro-assets ./public/swf ./storage - ./public/nitro-assets:/app/public/nitro-assets - ./public/swf:/app/public/swf # Runtime uploaded media (persistent on the host). - ./storage:/app/storage # ── Shared gamedata (absolute path the CMS hardcodes & writes to) ── # src/lib/services/furni-asset-dirs.ts: `DEFAULT_GAMEDATA_ROOT = # /var/www/Gamedata`. nginx on the host also serves /gamedata/ from this # same directory, so mount it into the container at the same absolute path. # Must be RW so furniture/badge imports can write mirrors to it. - /var/www/Gamedata:/var/www/Gamedata # # ── node_modules corruption (§ host-sync) ── # pnpm node_modules must NEVER be a host bind-mount: shared-filesystem # sync (Docker Desktop gRPC-FUSE/VirtioFS, Unison, Syncthing) corrupts # pnpm's hardlinked store and .bin shims. The production image already # bakes node_modules in at build time and is NOT bind-mounted here. # For local dev use a *named volume* instead of a host bind: # - node_modules:/app/node_modules # and never share `node_modules` / `pnpm store` via the Docker bind. # On macOS add `:cached`/`:delegated` consistency labels per mount. healthcheck: test: ["CMD", "node", "-e", "fetch('http://localhost:3002/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"] interval: 30s timeout: 10s retries: 3 start_period: 40s mem_limit: 2g # ── Byparr (Cloudflare bypass for clone sources) ── # Solves Cloudflare/TLS-fingerprint blocks via a headless Chrome browser. # Drop-in successor to FlareSolverr. The CMS calls this on # http://localhost:8191 for sources that block Node's TLS fingerprint # (e.g. Leet.city). Used by src/lib/services/byparr.ts. byparr: image: ghcr.io/thephaseless/byparr:latest container_name: byparr network_mode: host restart: unless-stopped environment: - LOG_LEVEL=INFO mem_limit: 2g healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:8191/health"] interval: 30s timeout: 10s retries: 3 start_period: 30s # ── Opt-in: Octane-Renderer (Habbo avatar imager) ── # Serves /imaging on port 3030 (the CMS proxies /imaging to it). Renders # avatars into /var/www/Gamedata/habbo-imaging, so it needs RW access. # Disabled by default — uncomment to run the renderer as a container. # imager: # build: # context: /var/www/Octane-Renderer # container_name: epicnext-octane-renderer # ports: # - "3030:3030" # restart: unless-stopped # volumes: # - /var/www/Gamedata:/var/www/Gamedata # ── MariaDB "Turbo" (heavy JSON / bulk-loads) ── # Dedicated MariaDB tuned for >50 MB JSON imports. All tuning lives inline # in the service `command:` (bulk_insert_buffer_size, # innodb_flush_log_at_trx_commit=2, max_allowed_packet=512M, ...) so the # container configures itself — no external .cnf to mount or keep in sync. # Opt-in via profile so `docker compose up` keeps running the standalone # stack as today. # # Start: docker compose --profile db up -d (= pnpm db:up) # Note: host networking binds 127.0.0.1:3306 → STOP the host MariaDB # first (the app's .env DATABASE_URL points at localhost:3306). # Fixes the "Pulling schema from database..." hang: raise # net_read/net_write_timeout (done above) + stop imports while # running `pnpm db:generate` / drizzle-kit introspection. mariadb-turbo: image: mariadb:11 container_name: mariadb-turbo profiles: ["db"] network_mode: host restart: unless-stopped environment: # mariadb:11 uses MARIADB_*; MYSQL_* also works but is deprecated there. - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD:-root} - MARIADB_DATABASE=${MARIADB_DATABASE:-habbo} - MARIADB_USER=${MARIADB_USER:-cms} - MARIADB_PASSWORD=${MARIADB_PASSWORD:-cms} - MARIADB_AUTO_UPGRADE=1 # MariaDB tunes itself — the Official image appends these flags to mysqld, # no external .cnf file needed. command: [ # Charset "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci", # 50 MB JSON batches: raise the 16 MB default packet ceiling. "--max-allowed-packet=512M", "--net-buffer-length=1M", # Timeouts — fixes the "Pulling schema from database..." hang # (drizzle-kit introspection no longer starves behind big imports). "--connect-timeout=30", "--wait-timeout=3600", "--interactive-timeout=3600", "--net-read-timeout=600", "--net-write-timeout=600", # InnoDB: durability/performance trade-off for bulk writes. "--innodb-flush-log-at-trx-commit=2", "--innodb-buffer-pool-size=2G", "--innodb-buffer-pool-instances=4", "--innodb-log-file-size=1G", "--innodb-log-buffer-size=64M", "--innodb-flush-method=O_DIRECT", "--innodb-autoextend-increment=512", "--innodb-max-dirty-pages-pct=90", "--bulk-insert-buffer-size=512M", # Raise so the engine nearly never double-writes during a 50 MB load. "--innodb-doublewrite=0", # libaio/native_aio misbehaves in some containers; io_uring path is fine. "--innodb-use-native-aio=0", # Temp tables used when MariaDB scans JSON blobs cannot be indexed. "--tmp-table-size=256M", "--max-heap-table-size=256M", "--read-buffer-size=4M", "--read-rnd-buffer-size=16M", # Save ~1-2 GB RAM; bulk loads don't need the instrumentation. "--performance-schema=OFF", "--skip-name-resolve", ] volumes: # Named volume, NOT a host bind — shared-filesystem sync trashes InnoDB # files the same way it trashes pnpm's node_modules. Named volumes live # inside the container filesystem, so 50 MB of JSON writes never cross a # host-sync boundary. - mariadb-turbo-data:/var/lib/mysql healthcheck: # healthcheck.sh ships in the official mariadb image. test: ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized || mariadb-admin ping --silent"] interval: 10s timeout: 5s retries: 5 start_period: 30s mem_limit: 4g # Named volumes declared once; used by the MariaDB service above. volumes: mariadb-turbo-data: driver: local