Files
admin/scripts/books-vds-portainer-migration/README.md
vitya 141efe9759 scripts(books-vds-portainer-migration): Phase 1 adapter ready
Clone-adapted from .wiki/concepts/portainer-stack-management-vds.md § Migration script.

Diffs from VDS-infra version:
- PORTAINER_URL = portainer.kzntsv.site
- Auth via X-API-Key (PAT works, no JWT fallback needed)
- DIR prefix /usr/docker/<stack> (not /opt/stacks/<stack>)
- Down step via `docker rm -f` by compose-project label
  (bypasses docker-compose v1 ContainerConfig bug entirely; no compose binary on path required)
- Refuses traefik/portainer migration (management plane)

Idempotent: delete-then-create against existing stack by name on endpoint 1. Re-runs are safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:16:10 +03:00

4.2 KiB

books-vds-portainer-migration

Migrates SSH-compose stacks at /usr/docker/<stack>/ on books VDS (89.253.255.133) to Portainer-managed via API. Adapted from canonical pattern: .wiki/concepts/portainer-stack-management-vds.md.

Related task: books-vds-stacks-to-portainer.

Usage

# 1. Get PAT from pass
export BOOKS_PORTAINER_API_KEY=$(pass show books-vds/full-env BOOKS_PORTAINER_API_KEY)

# 2. Copy script to VDS
scp -i ~/.ssh/id_ed25519_books_ops migrate.sh root@89.253.255.133:/tmp/

# 3. Migrate one stack at a time, verify between
ssh -i ~/.ssh/id_ed25519_books_ops root@89.253.255.133 \
  "BOOKS_PORTAINER_API_KEY='$BOOKS_PORTAINER_API_KEY' bash /tmp/migrate.sh proxy-chain"

Ladder (lowest → highest blast radius)

  1. proxy-chain — internal-only, no traefik exposure → smoke for the script
  2. imgproxy — 2-container compose, serves imgproxy.kzntsv.site
  3. minio — read-mostly, books-api retries OK
  4. mongo — shared, used by books-api + books-task-runner. VERIFY retry-tolerance first
  5. books-db — MariaDB, books-api connection-pool reconnects
  6. elasticsearch — currently "not used" per user. Verify before recreate.

Smoke per stack (after each migration)

# proxy-chain — internal smoke (no public endpoint)
docker exec proxy-chain wget -qO- localhost:8000 || echo "$?"

# imgproxy — serves imgproxy.kzntsv.site
curl -sS -o /dev/null -w "%{http_code}\n" https://imgproxy.kzntsv.site/

# minio
curl -sS -o /dev/null -w "%{http_code}\n" https://minio.kzntsv.site/minio/health/live

# mongo
docker exec mongo mongo --quiet --eval 'db.adminCommand({ping:1}).ok' \
  -u root -p "$(pass show books-vds/full-env | grep MONGO_PWD)"

# books-db
docker exec books-db mariadb -uroot -p"$(pass show books-vds/full-env | grep BOOKS_DB_PWD)" -e 'SELECT 1'

# elasticsearch
docker exec elasticsearch curl -sS localhost:9200/_cluster/health | jq .status

What the script does

  1. Backup docker-compose.yml.bak-pre-portainer-migration-<date> (idempotent — keeps first backup).
  2. Transform compose: strip env_file: (defensive — books VDS has none), absolutize ./ paths to /usr/docker/<stack>/.
  3. Parse .env into API env array (no-op for books VDS — no .env files).
  4. Delete existing Portainer stack with same name on endpoint 1 (idempotency for re-runs).
  5. Down ad-hoc containers via docker rm -f by com.docker.compose.project label (bypasses docker-compose v1 ContainerConfig bug).
  6. POST /api/stacks/create/standalone/string?endpointId=1 — Portainer pulls images, creates network attachments, applies traefik labels through docker socket events.

Diffs from VDS-infra script (portainer-stack-management-vds.md)

Aspect VDS-infra books VDS
Portainer URL portainer.vds.kzntsv.site portainer.kzntsv.site
Endpoint ID 1 (same) 1
Stack dir prefix /opt/stacks/<stack>/ /usr/docker/<stack>/
Auth JWT (PAT 401'd) X-API-Key (PAT works)
Compose down cd $DIR && docker compose down docker rm -f by label (skips compose binary entirely)
env_file: strip required (most stacks had .env) defensive (books VDS has none)

Safety

  • Re-runnable. Delete-then-create idempotency means re-running for the same stack just rebuilds it (data binds preserved, names preserved, network re-attached).
  • Compose file preserved on disk (in /usr/docker/<stack>/ + dated backup). Portainer manages lifecycle but doesn't move the file.
  • Bind paths preserved/usr/docker/<stack>/data etc. stay at the same disk path. Backup pipeline (scripts/books-vds-backup-daily-kreknin/) doesn't need adjustment.
  • Skips traefik / portainer by name — refuses to migrate management plane.

Post-migration