diff --git a/README.md b/README.md index 2819ef2..c2c7c88 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,10 @@ binance-market — это **не часть игры**, а **общий инфр | **k3s-кластер** | VM `192.168.76.120`, ns `game` | — | | **Игровой бэк (боевой)** | TS-шлюз `updown-relayer`, сервис `testfront-gateway-smart-updown-relayer:8895` | оба домена: `/api`, `/ws` | | **Домены** | `smart-updown.binom.pw` (Unity-игра), `testfront.binom.pw` (тех-дашборд) | один и тот же бэк | -| **Фронт игры** | deployment `fun-game-front2` (nginx + Unity WebGL) | образ `images.binom.pw/fun/game-front2:<тег>` | -| **Ingress-пути** | **`smart-updown-api`** — `/api`,`/ws` (ВНЕ helm); `fun-game-front2-…` — только `/` | ADR-010 | +| **Статика игры (корень `smart-updown.binom.pw`)** | **nginx на Proxmox, LXC 152 `game-web` (192.168.76.152)** | `/opt/game/releases//`, активная — `/opt/game/current` (ADR-011) | +| **Фронт игры (сборка)** | репозиторий `fun-game-front2` → образ `images.binom.pw/fun/game-front2:<тег>` | из образа извлекается статика и кладётся на nginx (`tools/proxmox-nginx/`) | +| **Ingress-пути (куб)** | **`smart-updown-api`** — `/api`,`/ws` (ВНЕ helm); `fun-game-front2-…` — только `/` (уже не обслуживает домен) | ADR-010 | +| **Маршрутизация корня** | внешний Traefik VDS: `/api`,`/ws` → куб `192.168.76.120`; всё остальное → nginx `192.168.76.152` | ADR-011 | | **Solana localnet** | LXC 151 на Proxmox `192.168.76.66` | RPC `192.168.76.181:8899` | | **Контракт (в цепи)** | `9ALs…S1fCpf` (фантики, 4 инструкции) | прод-игра работает тут | | **Реестр образов** | `images.binom.pw` | — | diff --git a/TASKS.md b/TASKS.md index e53ac40..b5771f4 100644 --- a/TASKS.md +++ b/TASKS.md @@ -156,3 +156,40 @@ helm upgrade fun-game-front2 binom/fun-game-front2 -n game \ ## Ссылки (дополнено 2026-09-13) - Цена: `docs/PRICE-NATS.md`, диаграмма `docs/diagrams/price-flow.puml` - **Безопасность: `docs/SECURITY.md` — чек-лист `SEC-1…SEC-15` (аудит живого стенда 2026-09-13)** + +## ⭐ 2026-09-15 (2) — статика игры ушла из куба на nginx (LXC 152), версии каталогами + +**Где мы:** корень `smart-updown.binom.pw` раздаёт **nginx на Proxmox (LXC 152 `game-web`, +192.168.76.152)** со версионированием по каталогам; `/api` и `/ws` по-прежнему идут в куб +на TS-шлюз отдельным router'ом внешнего Traefik. Решение и разбор — **ADR-011** в `docs/DECISIONS.md`. + +**Как выпустить новую сборку игры (памятка — вместо helm upgrade фронта):** +```bash +# 1. CI репозитория fun-game-front2 собирает Unity WebGL и пушит образ +# images.binom.pw/fun/game-front2: (тег = номер релиза, как раньше) +# 2. раскладываем на nginx и включаем как активную: +ssh root@192.168.76.66 "pct exec 152 -- /opt/game/tools/publish-release.sh --activate" +# 3. проверяем ДОМЕН (не helm): +curl -sI https://smart-updown.binom.pw/ | grep -iE 'server|last-modified' # nginx/1.24.0 (Ubuntu) +curl -s -o /dev/null -w '%{http_code}\n' https://smart-updown.binom.pw//Build/WebGL.framework.js +``` +Без `--activate` сборка просто ложится в `/opt/game/releases//` и доступна по `//` — +корень не меняется. Откат версии = `publish-release.sh <старый N> --activate` (каталоги не удаляются). + +**Инструменты:** `tools/proxmox-nginx/` — `unpack-release.sh` (распаковать dir-дамп образа), +`publish-release.sh` (тянуть образ + разложить + переключить), `game.nginx.conf`, +`smart-updown.traefik.yaml` (боевой конфиг внешнего Traefik). Скопированы в LXC 152 +в `/opt/game/tools/` и `/etc/nginx/sites-available/game`. + +**Проверено живьём:** `/` → 200 `nginx/1.24.0 (Ubuntu)`; `/27/`,`/28/` → 200, ассеты версий +различаются; `/api/state` → 200 JSON; `/api/ws` → цена идёт; сквозной `autoclose-check.cjs` — **9/9**. + +**Задачи на будущее по этой схеме:** +- [ ] **TODO** `[pump.meta]` Ссылки на версии — **только со слэшем** (`/28/`): голый `/28` даёт 301. + Если понадобится бесслэшевый вид — добавить в nginx `return 301 /$build/`. +- [ ] **TODO** `[pump.meta]` Чистка старых сборок на 152: каталоги `releases/` не удаляются сами + (~68 МБ на сборку). Политика ретеншена (сколько версий держим) — за владельцем. +- [ ] **TODO** `[pump.meta]` Автоматизировать шаг публикации в CI `fun-game-front2`: после push образа + дёргать `publish-release.sh` на 152 (сейчас — ручной вызов одной командой). +- [ ] **TODO** `[pump.meta]` Уточнить, нужен ли под `fun-game-front2` в кубе вообще (домен он больше + не обслуживает) — либо оставить как «предыдущая схема на откат», либо выключить. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index d65ef89..cb55d07 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -203,3 +203,58 @@ (отдать фронтендеру, см. `TASKS.md`). --- + +## ADR-011 — Статика игры вынесена из куба на nginx (LXC 152), версии — каталогами (2026-09-15) + +**Контекст.** До этого и корень, и `/api`,`/ws` жили в кубе: внешний Traefik (VDS) вёл весь домен +`smart-updown.binom.pw` на k3s `192.168.76.120`, статику отдавал под `fun-game-front2` +(nginx внутри образа), API — отдельный ingress `smart-updown-api` → TS-шлюз `:8895`. +Владелец решил **разделить плоскости**: бэк честно деплоится в куб, а сама игра (статика) +раздаётся с nginx, развёрнутого на Proxmox — со **версионированием по каталогам** (`/<номер>/`). + +**Решение:** +1. **nginx на Proxmox** — новый LXC **152** (`game-web`, 192.168.76.152, Ubuntu 24.04, 2 ядра, + 1 ГБ RAM, диск 8 GB на `local-lvm`, `onboot=1`, unprivileged). Отдельный LXC потому, что + статика игры — «10 строк конфига и каталог», держать её в кубе рядом с бэком смысла нет. +2. **Версионирование каталогами:** + - `/opt/game/releases//` — файлы сборки N (`Build/`, `TemplateData/`, `index.html`); + - `/opt/game/current` — симлинк на активную сборку; + - `https://smart-updown.binom.pw/` → активная сборка, `/28/…` → конкретная сборка. +3. **Маршрутизация на внешнем Traefik (VDS)** — два router'а на один host, вместо одного: + - `smart-updown-api-router`: `Host(...) && (PathPrefix('/api') || PathPrefix('/ws'))` → `192.168.76.120` (куб); + - `smart-updown-router`: `Host(...)` → `192.168.76.152` (nginx на проксмоксе). + PathPrefix-роутер специфичнее Host-роутера, поэтому `/api`,`/ws` **не перехватываются статикой**. + IP LXC 152 достижим с VDS через wg-туннель (192.168.76.0/22 dev wg0). +4. **В кубе ничего не менялось**: `ingress smart-updown-api` (`/api`,`/ws` → шлюз) как был, вне helm. + Под `fun-game-front2` остался жив, но домен он больше не обслуживает (статика ушла на nginx) — + в куб его релизить больше не обязательно; релизы — только доставка статики на nginx. +5. **Инструменты** (`tools/proxmox-nginx/`): `unpack-release.sh` (распаковка dir-дампа образа в + `releases/`), `publish-release.sh` (тянет образ из реестра, раскладывает, по `--activate` + переключает `current`), `game.nginx.conf`, `smart-updown.traefik.yaml`. + +**Питфоллы, пойманные живьём:** +- `skopeo copy docker://… dir:` кладёт слои по **sha256-digest**, а `manifest.json` — это **OCI-манифест** + (словарь `layers[].digest`, не список строк как в docker-archiv). Парсить `json['layers'][i]['digest']`, + а не печатать элемент целиком (иначе `tar: Cannot connect to {'mediaType': ...}`). +- Голый шаблон `/28` (без слэша) отдаёт **301** — это `try_files ... /$build$subpath/`, т.е. редирект + на `/28/`. Для относительных путей Unity (`src="Build/WebGL.loader.js"`) важно, чтобы **браузерный + URL заканчивался слэшем**; иначе `Build/...` уедет в `/Build/...` и вернётся 404. Ссылаться на + версии только со слэшем (`/28/`), либо ставить редирект в nginx (`return 301 /$build/`). +- `/solana-rpc/` **не** перехватывается PathPrefix `/api` — это отдельный path, и на VDS он уходит + в корневой router (nginx). Поэтому страховочный `location /solana-rpc/` в nginx нужен: старые сборки + игры стучатся именно туда (проверено: `getHealth` → `ok` через домен). +- Имена ассетов Unity внутри сборки не меняются (`WebGL.data`/`.wasm`/`.framework.js`), но теперь + URL содержит номер сборки → `immutable`-кэш больше не «прилипает» между релизами (закрыт остаточный + риск из ADR-010). Внутри одной версии ассеты неизменяемы, т.к. каталог версии не перезаписывается. + +**Приёмка (живой домен, 2026-09-15):** корень `/` → 200 от `nginx/1.24.0 (Ubuntu)` (LXC 152); +`/27/`,`/28/` → 200, ассеты версий различаются (md5 `WebGL.data`: `76b6f3…` vs `47453d…`); +`/api/state` → 200 JSON (куб), `/api/ws` → `[open]` + `{"type":"price"}`; сквозной +`tools/autoclose-check.cjs` на `BASE=https://smart-updown.binom.pw/api` — **9/9**; `testfront` не задет. +Откат: вернуть `/opt/traefik/static/smart-updown.yaml` из бэкапа VDS +(`/root/bak-smart-updown-nginx-2026-09-15/`) — домен снова целиком уйдёт в куб. + +**Альтернативы:** (а) оставить статику в кубе и версионировать каталогами внутри образа — тянет +пересборку образа на каждый билд игры и не решает вопрос кэша; (б) поднять в LXC не nginx, а +podman-контейнер с nginx — лишний слой на 10 строках конфига; (в) отдавать статику прямо с k3s-Traefik +по PVC — не то, о чём просил владелец (он хотел nginx на проксмоксе). diff --git a/tools/proxmox-nginx/README.md b/tools/proxmox-nginx/README.md new file mode 100644 index 0000000..642bca3 --- /dev/null +++ b/tools/proxmox-nginx/README.md @@ -0,0 +1,56 @@ +# nginx на Proxmox — раздача статики игры (LXC 152) + +Боевой веб-слой игры «Бинарная ракета»: **корень `smart-updown.binom.pw` раздаётся nginx'ом +на LXC 152 с версионированием по каталогам**. Бэк (API/WS) остаётся в кубе — см. ADR-011. + +## Что где живёт + +| Что | Где | +|---|---| +| LXC | **152 `game-web`**, 192.168.76.152 (Server3, `pct exec 152` через `root@192.168.76.66`) | +| nginx-конфиг | `/etc/nginx/sites-available/game` (симлинк в `sites-enabled/`) | +| Сборки | `/opt/game/releases//` — `Build/`, `TemplateData/`, `index.html`, `ServiceWorker.js` | +| Активная сборка | `/opt/game/current` → симлинк на `releases/` | +| Инструменты | `/opt/game/tools/` (`unpack-release.sh`, `publish-release.sh`) | +| Логи | `/var/log/nginx/game-access.log`, `game-error.log` | + +## Адреса + +- `https://smart-updown.binom.pw/` — активная сборка; +- `https://smart-updown.binom.pw//` — конкретная сборка (**обязательно со слэшем**); +- `https://smart-updown.binom.pw//Build/WebGL.data` — ассет конкретной сборки; +- `/api`, `/ws` — **не nginx**: их забирает внешний Traefik (VDS) → куб (ingress `smart-updown-api`). + +## Публикация сборки + +```bash +# CI fun-game-front2 пушит образ images.binom.pw/fun/game-front2: +# затем на хосте Proxmox: +ssh root@192.168.76.66 "pct exec 152 -- /opt/game/tools/publish-release.sh --activate" +``` + +Без `--activate` сборка ляжет в `/opt/game/releases//` и будет доступна по `//`, +но корень останется прежним. Откат = `publish-release.sh <старый N> --activate`. + +## Проверка + +```bash +curl -sI https://smart-updown.binom.pw/ | grep -iE 'server|last-modified' # nginx/1.24.0 (Ubuntu) +curl -s -o /dev/null -w '%{http_code}\n' https://smart-updown.binom.pw// +curl -s https://smart-updown.binom.pw/api/state | head -c 80 # JSON от шлюза в кубе +# сквозная живая приёмка (ставит реальную ставку): +cd ~/WORK/games/pump-game-ops && BASE=https://smart-updown.binom.pw/api \ + WS_URL=wss://smart-updown.binom.pw/api/ws node tools/autoclose-check.cjs # ждём 9/9 +``` + +## Питфоллы + +- **Слэш в конце обязателен** для версии: `/28` → 301 (редирект на `/28/`). Unity грузит ассеты + относительным путём (`Build/WebGL.loader.js`), поэтому без слэша `Build/...` уедет в корень → 404. +- **`skopeo ... dir:` пишет OCI-манифест**, где `layers` — словари. `unpack-release.sh` берёт + `digest` каждого слоя и распаковывает по имени файла `` (не `sha256:...`). +- **Кэш больше не прилипает между релизами**: URL содержит номер сборки → `immutable` корректен. + Внутри версии каталог не перезаписывается, поэтому ассеты действительно неизменяемы. +- **Сборки копятся** (~68 МБ на версию) — ретеншен пока ручной, см. `TASKS.md`. +- **Откат схемы целиком**: вернуть `/opt/traefik/static/smart-updown.yaml` из + `/root/bak-smart-updown-nginx-2026-09-15/` на VDS — домен снова пойдёт целиком в куб. diff --git a/tools/proxmox-nginx/game.nginx.conf b/tools/proxmox-nginx/game.nginx.conf new file mode 100644 index 0000000..edc4f88 --- /dev/null +++ b/tools/proxmox-nginx/game.nginx.conf @@ -0,0 +1,98 @@ +# nginx для раздачи Unity-сборок «Бинарной ракеты» — LXC 152 (game-web, 192.168.76.152) +# Файл: /etc/nginx/sites-available/game +# +# Модель версионирования: +# /opt/game/releases// — файлы сборки N (Build/, TemplateData/, index.html) +# /opt/game/current -> releases/ — активная сборка (симлинк, переключает publish-release.sh) +# +# https://smart-updown.binom.pw/ -> активная сборка (current) +# https://smart-updown.binom.pw/28/ -> конкретная сборка 28 +# https://smart-updown.binom.pw/28/Build/WebGL.data -> ассет сборки 28 +# +# API и WebSocket nginx НЕ обслуживает: их забирает Traefik на VDS +# (router smart-updown-api: PathPrefix /api,/ws -> куб 192.168.76.120). +# Локальные location /api, /ws, /solana-rpc — страховка для запросов, +# которые всё же дойдут до nginx (прямой заход по IP, старые сборки). + +# Ассеты Unity внутри каталога версии неизменяемы -> immutable. +# index.html и всё остальное -> no-store. +map $uri $game_cache_control { + default "no-store"; + ~^/[0-9]+/.+\.(js|wasm|data|mem|symbols|css|png|jpg|jpeg|gif|svg|woff2?)$ "public, max-age=604800, immutable"; + ~^/.+\.(js|wasm|data|mem|symbols|css|png|jpg|jpeg|gif|svg|woff2?)$ "public, max-age=604800, immutable"; +} + +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + root /opt/game/current; + index index.html; + + access_log /var/log/nginx/game-access.log; + error_log /var/log/nginx/game-error.log warn; + + gzip on; + gzip_min_length 512; + gzip_types application/javascript application/wasm application/json text/css application/octet-stream; + + add_header Cache-Control $game_cache_control; + + # --- версионированные сборки: /<число>/... ------------------------------- + # /28/index.html -> /opt/game/releases/28/index.html + location ~ ^/(?[0-9]+)(?/.*)?$ { + root /opt/game/releases; + try_files /$build$subpath /$build$subpath/ =404; + } + + # --- ассеты активной сборки ---------------------------------------------- + location ~* \.(js|wasm|data|mem|symbols|json|css|png|jpg|jpeg|gif|svg|woff2?)$ { + root /opt/game/current; + try_files $uri =404; + } + + # --- страховка: API ------------------------------------------------------- + # Нормальный /api идёт мимо nginx (Traefik VDS -> куб). Если запрос всё же + # пришёл сюда — уходим в куб на k3s-Traefik с правильным Host, дальше + # ingress smart-updown-api ведёт в шлюз :8895. + location /api { + proxy_pass http://192.168.76.120; + proxy_http_version 1.1; + proxy_set_header Host smart-updown.binom.pw; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_connect_timeout 10s; + proxy_read_timeout 90s; + } + + # страховка: WebSocket шлюза + location /ws { + proxy_pass http://192.168.76.120; + proxy_http_version 1.1; + proxy_set_header Host smart-updown.binom.pw; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 3600s; + } + + # страховка: тонкий CORS-прокси до Solana RPC (старые сборки игры) + location /solana-rpc/ { + proxy_pass http://192.168.76.181:8899/; + proxy_http_version 1.1; + proxy_set_header Host 192.168.76.181:8899; + proxy_connect_timeout 5s; + proxy_read_timeout 30s; + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Headers "content-type" always; + add_header Access-Control-Allow-Methods "POST, OPTIONS" always; + if ($request_method = OPTIONS) { return 204; } + } + + # --- корень = активная сборка -------------------------------------------- + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/tools/proxmox-nginx/publish-release.sh b/tools/proxmox-nginx/publish-release.sh new file mode 100644 index 0000000..9125604 --- /dev/null +++ b/tools/proxmox-nginx/publish-release.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Публикация сборки игры на nginx (LXC 152, game-web). +# +# publish-release.sh <тег> [--activate] +# +# Что делает: +# 1. тянет образ images.binom.pw/fun/game-front2:<тег> из реестра (skopeo copy в dir); +# 2. распаковывает статику в /opt/game/releases/<тег>/; +# 3. без --activate оставляет активной текущую сборку (корень не меняется); +# с --activate переключает симлинк /opt/game/current на новую версию; +# 4. печатает проверку. +# +# Итог: https://smart-updown.binom.pw/ -> активная сборка +# https://smart-updown.binom.pw/<тег>/ -> конкретная сборка +set -euo pipefail + +TAG="${1:?использование: publish-release.sh <тег> [--activate]}" +ACTIVATE="${2:-}" +REG="images.binom.pw/fun/game-front2" +CREDS="subochev:Drovosek319" +IMG="/tmp/gameimg-${TAG}" + +echo "=== 1/4 тяну образ ${REG}:${TAG} ===" +rm -rf "$IMG" +skopeo copy --src-creds "$CREDS" "docker://${REG}:${TAG}" "dir:${IMG}" +# креды не нужны при распаковке +chmod -R a+rX "$IMG" + +echo "=== 2/4 распаковываю статику в /opt/game/releases/${TAG} ===" +/opt/game/tools/unpack-release.sh "$TAG" "$IMG" + +if [ "$ACTIVATE" = "--activate" ]; then + echo "=== 3/4 переключаю активную сборку на ${TAG} ===" + PREV="$(readlink -f /opt/game/current || echo none)" + rm -f /opt/game/current + ln -s "/opt/game/releases/${TAG}" /opt/game/current + echo "было: $PREV" + echo "стало: $(readlink -f /opt/game/current)" +else + echo "=== 3/4 активную сборку НЕ трогаю (нет --activate) ===" + echo "активна: $(readlink -f /opt/game/current)" +fi + +echo "=== 4/4 проверка ===" +systemctl reload nginx +for u in "/" "/${TAG}/" "/${TAG}/index.html" "/${TAG}/Build/WebGL.framework.js"; do + printf ' %-42s %s\n' "$u" "$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1${u}")" +done +echo "готово: сборка ${TAG}" +rm -rf "$IMG" diff --git a/tools/proxmox-nginx/smart-updown.traefik.yaml b/tools/proxmox-nginx/smart-updown.traefik.yaml new file mode 100644 index 0000000..a5c719a --- /dev/null +++ b/tools/proxmox-nginx/smart-updown.traefik.yaml @@ -0,0 +1,62 @@ +http: + # ── middlewares ───────────────────────────────────────────────────────── + middlewares: + # Подмена Host на входе в куб — k3s-Traefik роутит именно по Host, + # а ingress `smart-updown-api` объявлен на smart-updown.binom.pw. + smart-updown-host: + headers: + customRequestHeaders: + Host: "smart-updown.binom.pw" + smart-updown-redirect-https: + redirectScheme: + scheme: https + permanent: true + + # ── routers ───────────────────────────────────────────────────────────── + routers: + # API и WebSocket игры: уходят в k3s (192.168.76.120) на ingress + # `smart-updown-api` (ns game), дальше в TS-шлюз :8895. + # Отдельный router потому, что PathPrefix специфичнее Host: + # так /api и /ws не перехватываются статикой nginx. + smart-updown-api-router: + rule: "Host(`smart-updown.binom.pw`) && (PathPrefix(`/api`) || PathPrefix(`/ws`))" + service: smart-updown-api + entryPoints: + - websecure + tls: + certResolver: mycloudflare + + # Корень: статика игры. Раздаёт nginx на LXC 152 (game-web, 192.168.76.152) + # в домашней сети через wg-туннель. Версии — каталогами (/28/, /29/, ...), + # корень — активная сборка (симлинк /opt/game/current). + smart-updown-router: + rule: Host(`smart-updown.binom.pw`) + service: smart-updown + entryPoints: + - websecure + tls: + certResolver: mycloudflare + + # http -> https + smart-updown-http-redirect: + rule: Host(`smart-updown.binom.pw`) + entryPoints: + - web + service: noop + middlewares: + - smart-updown-redirect-https + + # ── services ──────────────────────────────────────────────────────────── + services: + smart-updown-api: + loadBalancer: + servers: + - url: "http://192.168.76.120" + smart-updown: + loadBalancer: + servers: + - url: "http://192.168.76.152" + noop: + loadBalancer: + servers: + - url: "http://127.0.0.1:9" diff --git a/tools/proxmox-nginx/unpack-release.sh b/tools/proxmox-nginx/unpack-release.sh new file mode 100644 index 0000000..747de2b --- /dev/null +++ b/tools/proxmox-nginx/unpack-release.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Распаковка образа игры из dir-дампа skopeo в версионированный каталог nginx. +# Использование: unpack-release.sh <тег> +set -euo pipefail + +TAG="$1" +IMG="$2" +DEST="/opt/game/releases/${TAG}" + +[ -d "$IMG" ] || { echo "нет дампа $IMG"; exit 1; } + +rm -rf /tmp/rootfs-${TAG} +mkdir -p /tmp/rootfs-${TAG} + +cd "$IMG" +# слои из manifest.json в правильном порядке +/usr/bin/python3 - <<'PY' > /tmp/layers-${TAG}.txt +import json +m = json.load(open('manifest.json')) +for l in m['layers']: + print(l['digest'].split(':', 1)[1]) +PY + +while read -r layer; do + [ -z "$layer" ] && continue + case "$layer" in + *tar.gz|*tgz) tar -xzf "$layer" -C /tmp/rootfs-${TAG} ;; + *tar.zst) tar --zstd -xf "$layer" -C /tmp/rootfs-${TAG} ;; + *) tar -xf "$layer" -C /tmp/rootfs-${TAG} ;; + esac +done < /tmp/layers-${TAG}.txt + +SRC="/tmp/rootfs-${TAG}/usr/share/nginx/html" +[ -d "$SRC" ] || { echo "нет $SRC в образе"; ls /tmp/rootfs-${TAG}/usr/share/nginx 2>/dev/null; exit 1; } + +mkdir -p "$DEST" +cp -a "$SRC/." "$DEST/" +chmod -R a+rX /opt/game/releases +rm -rf /tmp/rootfs-${TAG} /tmp/layers-${TAG}.txt + +echo "=== релиз ${TAG} разложен в ${DEST} ===" +ls -la "$DEST" +du -sh "$DEST"