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:
119
.wiki/concepts/portainer-stack-management-vds.md
Normal file
119
.wiki/concepts/portainer-stack-management-vds.md
Normal 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.
|
||||
@@ -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
|
||||
- [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-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)
|
||||
- [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)
|
||||
|
||||
@@ -11,3 +11,9 @@ follow project discipline
|
||||
delegate to interns when allowed
|
||||
recommend, don't menu
|
||||
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).
|
||||
|
||||
Reference in New Issue
Block a user