# Shared image repository `icons/` is served as `/image/icons/`. General portraits follow these rules: - `icons/장수/<장수명>.<확장자>` is the canonical shared path. - Scenario-specific collections keep a separate directory such as `icons/걸그룹/` or `icons/롤시나리오/`. - Root-level numeric files such as `icons/1047.jpg` are compatibility aliases for deployed legacy versions. Do not delete or replace them while those versions are in service. - A general name must map to one canonical file. If two records intentionally need different portraits, disambiguate the scenario name or use an explicit scenario-specific path. The core2026 repository owns `resources/general-icons.json` and `tools/manage-general-icons.mjs`. Use that tool with this repository as `--image-root` to synchronize aliases, scenario paths, and verify that every catalog source exists and has identical bytes. ## Public URLs The same tracked files are exposed through both URL contracts: - `https://sam.hided.net/image/game/...` and `/image/icons/...` - `https://sam-image.hided.net/game/...` and `/icons/...` - `https://sam-image.hided.net/image/...` is a compatibility alias. Do not redirect the old `/image/*` URLs to the dedicated domain. Existing PHP and core2026 clients use same-origin relative paths and can move independently. The image server never exposes the repository root, `.git`, PHP sources, or directory listings. ## Node webhook deployment service `compose.yaml` runs a read-only Nginx static edge and an internal Node service. Only the edge publishes port 8191. The Node service verifies the raw Gitea `X-Gitea-Signature`, accepts push events for `devsam/image`, fetches the exact remote branch tip, rejects dirty or non-fast-forward updates, and serializes all Git changes. The initial and only allowed branch is `master` unless `IMAGE_ALLOWED_BRANCHES` is explicitly expanded. The current Caddy host reaches this server from `172.30.1.75`. Keep the observed source in the untracked `.env`; re-check it whenever Caddy networking changes. The published port binds all server interfaces, so production also needs a host `DOCKER-USER` (or equivalent) firewall rule allowing that source CIDR to TCP 8191 and rejecting other sources. Nginx applies the same source allowlist. ### Prepare ```sh cp .env.example .env ./deploy/scripts/init-secrets.sh docker compose config --quiet docker compose build ``` Apply and inspect the dedicated Docker ingress chain with root privileges: ```sh sudo env CADDY_SOURCE_CIDR=172.30.1.75/32 IMAGE_PORT=8191 \ ./deploy/scripts/firewall-8191.sh apply sudo ./deploy/scripts/firewall-8191.sh check ``` The script only owns the `SAM_IMAGE_INGRESS` chain and its port-8191 jump from `DOCKER-USER`; it does not flush shared firewall chains. Secret values are generated under ignored `secrets/` files with mode 0600 and are never printed. Put the contents of `secrets/gitea_webhook_secret` into the Gitea webhook configuration. Configure a JSON push webhook targeting: ```text https://sam-image.hided.net/v1/hooks/gitea ``` Use branch filter `master`. Disable the old PHP webhook before enabling the new writer. The legacy PHP files remain in `hook/` for an explicit rollback, but PHP and Node must never mutate the checkout concurrently. Legacy HTTP mutation is disabled by default. An emergency PHP rollback must first stop `image-hook`, then create the ignored `hook/legacy-enabled` sentinel in the legacy checkout before restoring its Caddy/Gitea route. Remove the sentinel before Node is started again. CLI execution of `hook/git_pull.php` from the `hook/` directory remains available for local recovery without exposing the HTTP endpoint. ### Start and verify ```sh docker compose up -d --build docker compose ps curl -fsS https://sam-image.hided.net/healthz curl -fsS https://sam-image.hided.net/v1/status curl -fsS https://sam-image.hided.net/game/back.jpg -o /dev/null curl -fsS https://sam-image.hided.net/image/icons/default.jpg -o /dev/null ``` Nginx serves only `game/`, `icons/`, `hook/list.json`, and `hook/inventory.v2.json`. The API inventory additionally reports the deployed branch, full commit, generation time, asset list, and both public base URLs. ### Explicit branch deployment Automatic pushes only update the active branch. Add a branch to `IMAGE_ALLOWED_BRANCHES`, recreate `image-hook`, and switch it with the internal signed administration command: ```sh ./deploy/scripts/admin-deploy.sh [expected-commit] ``` The administration route is not proxied through Nginx or Caddy. ### Tests and rollback ```sh docker build -t sam-image-hook:test node-hook docker run --rm sam-image-hook:test npm test docker compose exec -T image-web nginx -t -c /tmp/nginx.conf ``` Stopping `image-hook` does not remove the last checked-out files from the static service. For a full rollback, stop Node mutation first, restore the old Caddy/PHP webhook route, and only then enable the legacy writer. Preserve both checkouts until public file hashes and representative browser requests have been verified.