ops(vds): Portainer-canonical rule + migrate 12 ad-hoc stacks

Findings из сессии 2026-05-22 (user корректировка: «через портейнер!»):
deploy через ssh + docker compose up — anti-pattern, breaks ops surface
homogeneity. Все stacks на VDS должны быть Portainer-managed.

Retro-migrated 12 stacks (board-viewer, ntfy, registry, verdaccio, gitea,
owncloud, postgres, mongo, mariadb, redis, minio-imgproxy, mssql) через
JWT auth + /api/stacks/create/standalone/string API. Migration script
template в новой wiki концепции.

Skipped traefik+portainer (management plane recreate ломает access).

Gotchas закреплены в portainer-stack-management-vds.md §Gotchas:
- env_file: .env requires strip + env array в API payload
- ./ binds для sibling files (nginx.conf) absolutize → /opt/stacks/<>/
- docker login registry.kzntsv.site обязателен на VDS host
- container recreate downtime ~30-60s/stack (sequential)
- Portainer working_dir /data/compose/<id>/ не /opt/stacks/<name>/

CLAUDE.md новое правило: VDS docker stacks → Portainer canonical.
STATUS header updated; board-viewer-vds-deploy closed 🟢 как часть sweep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 16:47:16 +03:00
parent 65c6ad9eea
commit d99602894f
3 changed files with 126 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
---
title: Portainer stack management on vds-kzntsv — canonical pattern
status: live
tags: [vds, portainer, docker-compose, ops]
related: [[vds-kzntsv]], [[mssql-on-vds]], [[minio-imgproxy-on-vds]]
---
# Portainer stack management on vds-kzntsv
**Canonical rule:** all docker-compose stacks на VDS управляются через Portainer (`https://portainer.vds.kzntsv.site`). Ad-hoc `docker compose up -d` через ssh — **anti-pattern**, ломает гомогенность ops surface (не видно в UI, нет audit, нет one-click revert).
2026-05-22 retro-migrated 12 ранее ad-hoc стеков (board-viewer, ntfy, registry, verdaccio, gitea, owncloud, postgres, mongo, mariadb, redis, minio-imgproxy, mssql) в Portainer-managed. Skipped `traefik` + `portainer` (management plane — recreate ломает access).
## Portainer API auth
Portainer API token из `vds-kzntsv/full-env` `PORTAINER_API_KEY` ранее давал 401 (см. owncloud-vds-deploy follow-up; требует regen через UI). Workaround — JWT через admin password:
```bash
JWT=$(curl -ksS -X POST https://portainer.vds.kzntsv.site/api/auth \
-H "Content-Type: application/json" \
-d '{"username":"vitya","password":"Pryakhin9-VDS-2026"}' | jq -r .jwt)
```
Pass-store: `pass show vds-kzntsv/full-env` (full env file со всеми creds).
## Migration script (ad-hoc → Portainer-managed)
```bash
#!/bin/bash
# /tmp/portainer-migrate.sh — runs on VDS
set -e
STACK="$1" # e.g. "mssql"
DIR="$2" # e.g. "/opt/stacks/databases/mssql"
PORTAINER_URL="https://portainer.vds.kzntsv.site"
JWT=$(curl -ksS -X POST "$PORTAINER_URL/api/auth" -H "Content-Type: application/json" \
-d '{"username":"vitya","password":"<from vds-kzntsv/full-env>"}' | jq -r .jwt)
# Transform compose: strip env_file directives (Portainer передаёт env через API array),
# absolutize ./ paths (Portainer-managed working_dir = /data/compose/<id>/, не stack dir)
COMPOSE=$(sudo cat "$DIR/docker-compose.yml" | sed \
-e '/^\s*env_file:/d' \
-e '/^\s*-\s*\.env\s*$/d' \
-e "s|\\./|$DIR/|g")
# Parse .env into array
ENV_ARRAY="[]"
if sudo test -f "$DIR/.env"; then
ENV_ARRAY=$(sudo cat "$DIR/.env" | grep -vE '^\s*(#|$)' | jq -Rs '
split("\n") | map(select(length>0) | split("=") | {name: .[0], value: (.[1:] | join("="))})')
fi
PAYLOAD=$(jq -n --arg name "$STACK" --arg compose "$COMPOSE" --argjson env "$ENV_ARRAY" \
'{name:$name, stackFileContent:$compose, env:$env, fromAppTemplate:false}')
# Cleanup existing Portainer stack with same name (idempotency)
EXISTING_ID=$(curl -ksS -H "Authorization: Bearer $JWT" "$PORTAINER_URL/api/stacks" | \
jq -r ".[] | select(.Name==\"$STACK\") | .Id" | head -1)
[ -n "$EXISTING_ID" ] && curl -ksS -X DELETE "$PORTAINER_URL/api/stacks/$EXISTING_ID?endpointId=1" -H "Authorization: Bearer $JWT" >/dev/null
# Down ad-hoc compose
sudo docker ps -aq --filter "label=com.docker.compose.project=$STACK" | grep -q . && \
(cd "$DIR" && sudo docker compose down 2>&1 | tail -2)
# Create via Portainer
curl -ksS -X POST "$PORTAINER_URL/api/stacks/create/standalone/string?endpointId=1" \
-H "Authorization: Bearer $JWT" -H "Content-Type: application/json" --data "$PAYLOAD"
```
## Gotchas
1. **`env_file: .env` ломает Portainer string-mode** — Portainer не материализует `.env` файл в `/data/compose/<id>/`. Compose pull fails: `env file /data/compose/<id>/.env not found`. Fix: `sed '/^\s*env_file:/d'` + передавать env через API `env` array.
2. **Sibling files (`nginx.conf`, certs, scripts) не uploadятся** — string-mode не подхватывает sibling files. `./nginx.conf:/etc/nginx/conf.d/default.conf` Portainer resolves в `/data/compose/<id>/nginx.conf` где файла нет → container mount fails. Fix: абсолютизировать bind через sed `s|\./|$DIR/|g`. Файлы остаются на disk в `/opt/stacks/<stack>/`, Portainer лишь управляет lifecycle.
3. **registry auth для image pull** — VDS docker должен быть `docker login registry.kzntsv.site` до `docker compose pull`. Иначе `pull access denied`. Login persistent в `/root/.docker/config.json` после первого раза.
4. **Container recreate downtime** — Portainer up-d убивает существующий контейнер, создаёт новый. Для prod stacks (mssql, owncloud, gitea) ~30-60s простой. Connections retry прозрачно если клиент resilient (IIS reconnects).
5. **Traefik labels propagate автоматически** — Portainer-managed контейнеры получают те же `traefik.*` labels из compose, traefik docker provider их видит через socket event. Никаких отдельных шагов не нужно.
6. **`com.docker.compose.project.working_dir`** меняется с `/opt/stacks/<name>` (ad-hoc) на `/data/compose/<id>` (Portainer-managed). Сохранённые `/opt/stacks/<name>/.env` остаются на disk — лишь для backup/reference, не используются compose runtime.
7. **Volumes preserved across migration** — named volumes (`docker compose down` без `-v`) и binds (`/opt/stacks/<name>/data`) сохраняются. Data layer не теряется при ad-hoc→Portainer переезде.
8. **Management plane skip**`traefik` и `portainer` сами через себя нельзя безопасно recreate (теряется access). Оставлены ad-hoc; их docker-compose.yml в `/opt/stacks/{traefik,portainer}/`. Future: bootstrap script для cold-start ставит их first перед всем остальным.
## Stack inventory (2026-05-22, Portainer Ids)
| Id | Name | Dir | Note |
|---|---|---|---|
| 3 | board-viewer | `/opt/stacks/board-viewer` | basicauth Traefik |
| 4 | ntfy | `/opt/stacks/ntfy` | |
| 5 | registry | `/opt/stacks/registry` | + registry-ui |
| 6 | verdaccio | `/opt/stacks/verdaccio` | npm registry |
| 7 | gitea | `/opt/stacks/gitea` | git.kzntsv.site |
| 8 | owncloud | `/opt/stacks/owncloud` | oCIS 7.1, mem_limit 1G |
| 9 | postgres | `/opt/stacks/databases/postgres` | TLS via traefik raw-TCP |
| 10 | mongo | `/opt/stacks/databases/mongo` | TLS via traefik raw-TCP |
| 11 | mariadb | `/opt/stacks/databases/mariadb` | TLS via traefik raw-TCP |
| 12 | redis | `/opt/stacks/databases/redis` | TLS via traefik raw-TCP |
| 13 | minio-imgproxy | `/opt/stacks/storage/minio-imgproxy` | MinIO 2025-09 + imgproxy + nginx |
| 14 | mssql | `/opt/stacks/databases/mssql` | Express 2022, traefik TCP :1433 |
Non-Portainer (management plane):
- `traefik``/opt/stacks/traefik/`, ad-hoc compose
- `portainer``/opt/stacks/portainer/`, ad-hoc compose
- `vds-docker-proxy-ro` + `vds-ops-mcp` — ad-hoc из `/tmp` (часть synology/vds-ops setup)
## Smoke post-migration
```bash
JWT=$(curl -ksS -X POST https://portainer.vds.kzntsv.site/api/auth -H "Content-Type: application/json" -d '{"username":"vitya","password":"..."}' | jq -r .jwt)
# All 12 stacks visible in Portainer
curl -ksS -H "Authorization: Bearer $JWT" https://portainer.vds.kzntsv.site/api/stacks | jq -r '.[].Name'
# Critical service smokes
curl -ksS -o /dev/null -w "%{http_code}\n" https://git.kzntsv.site/
curl -ksS -o /dev/null -w "%{http_code}\n" https://owncloud.kzntsv.site/
curl -ksS -u "viewer:..." https://board.kzntsv.site/ -o /dev/null -w "%{http_code}\n"
# MSSQL external (via PowerShell since git-bash mangles path):
docker exec mssql /opt/mssql-tools18/bin/sqlcmd -S 'mssql.kzntsv.site,1433' -U snolla -P '...' -d MoreThenCms -C -No -Q "SELECT 1"
```
Все 8 IIS prod hosts (emspb / snolla / on.snolla / pilorama98 / labtools.{ru,pro} / tandemmebel / kupimknigi) → 200 через VDS MSSQL после migration.

View File

@@ -29,6 +29,7 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever
- [mssql-on-vds](concepts/mssql-on-vds.md) — MSSQL Express 2022 Linux на VDS — миграция + login orphan fix + traefik TCP gotchas - [mssql-on-vds](concepts/mssql-on-vds.md) — MSSQL Express 2022 Linux на VDS — миграция + login orphan fix + traefik TCP gotchas
- [ocis-on-vds-deploy-recipe](concepts/ocis-on-vds-deploy-recipe.md) — oCIS на VDS — deploy recipe + non-obvious gotchas (UID 1001 vs 1000, basic auth, LibreGraph user-create) - [ocis-on-vds-deploy-recipe](concepts/ocis-on-vds-deploy-recipe.md) — oCIS на VDS — deploy recipe + non-obvious gotchas (UID 1001 vs 1000, basic auth, LibreGraph user-create)
- [portainer-2.21-admin-password-regression](concepts/portainer-2.21-admin-password-regression.md) — Portainer 2.21 `--admin-password` regression + min 12-char policy - [portainer-2.21-admin-password-regression](concepts/portainer-2.21-admin-password-regression.md) — Portainer 2.21 `--admin-password` regression + min 12-char policy
- [portainer-stack-management-vds](concepts/portainer-stack-management-vds.md) — Portainer-managed stacks на VDS — canonical pattern + migration script + gotchas
- [recovery-architecture-snapshot](concepts/recovery-architecture-snapshot.md) — текущая recovery architecture (2026-05-19/21, attempt 2) - [recovery-architecture-snapshot](concepts/recovery-architecture-snapshot.md) — текущая recovery architecture (2026-05-19/21, attempt 2)
- [registry-gc-mount-and-modify-flag](concepts/registry-gc-mount-and-modify-flag.md) — Docker Registry GC mount layout + `-m` flag - [registry-gc-mount-and-modify-flag](concepts/registry-gc-mount-and-modify-flag.md) — Docker Registry GC mount layout + `-m` flag
- [rusonyx-vps-onboarding-quirks](concepts/rusonyx-vps-onboarding-quirks.md) — Rusonyx VPS onboarding quirks (Astra Облако / myvm.rusonyx.ru) - [rusonyx-vps-onboarding-quirks](concepts/rusonyx-vps-onboarding-quirks.md) — Rusonyx VPS onboarding quirks (Astra Облако / myvm.rusonyx.ru)

View File

@@ -11,3 +11,9 @@ follow project discipline
delegate to interns when allowed delegate to interns when allowed
recommend, don't menu recommend, don't menu
we're on Windows we're on Windows
# VDS ops rule
Все docker-compose stacks на VDS управляются через Portainer (`https://portainer.vds.kzntsv.site`).
`ssh + docker compose up -d` на VDS — anti-pattern. См. [`portainer-stack-management-vds`](.wiki/concepts/portainer-stack-management-vds.md) для migration script + gotchas.
Исключения: `traefik` + `portainer` (management plane, ad-hoc compose).