# 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 service is reverse-proxy agnostic. Caddy, Nginx, HAProxy, or another proxy can use the same port and path contract. For a native reverse proxy running on the same host as Docker, publish port 8191 on loopback only and proxy to `127.0.0.1:8191`: ```dotenv IMAGE_BIND_ADDRESS=127.0.0.1 IMAGE_PORT=8191 TRUSTED_PROXY_CIDRS=172.24.0.1/32 ``` These two addresses intentionally describe different hops. The native proxy connects to the host loopback address, while the Nginx process inside `image-web` normally observes the connection as coming from that Compose network's Docker bridge gateway (`172.24.0.1` in the example). Use the source shown in the `image-web` access/error log, then recreate `image-web` whenever `TRUSTED_PROXY_CIDRS` changes. Do not replace it with a Cloudflare CIDR: the direct peer of `image-web` remains the host-native reverse proxy path. If the reverse proxy is on another host, bind an explicit private interface instead of loopback and apply the host `DOCKER-USER` (or equivalent) firewall rule. Keep every source CIDR observed by `image-web` in the untracked `TRUSTED_PROXY_CIDRS` value, separated by commas or spaces. Avoid `0.0.0.0` unless the firewall has already been verified. ### Prepare ```sh cp .env.example .env ./deploy/scripts/init-secrets.sh docker compose config --quiet docker compose build ``` Only for a proxy on another host, apply and inspect the dedicated Docker ingress chain with root privileges: ```sh sudo env TRUSTED_PROXY_CIDRS=192.0.2.10/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. For the same-host native-proxy case, point the proxy upstream at loopback. A minimal Caddy site is: ```caddyfile sam-image.hided.net { reverse_proxy 127.0.0.1:8191 } ``` The equivalent Nginx/HAProxy configuration should use the same upstream. The application does not depend on Caddy-specific request behavior. 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 exposed through the public reverse proxy. ### 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.