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>
4.2 KiB
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)
- proxy-chain — internal-only, no traefik exposure → smoke for the script
- imgproxy — 2-container compose, serves
imgproxy.kzntsv.site - minio — read-mostly, books-api retries OK
- mongo — shared, used by books-api + books-task-runner. VERIFY retry-tolerance first
- books-db — MariaDB, books-api connection-pool reconnects
- 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
- Backup
docker-compose.yml.bak-pre-portainer-migration-<date>(idempotent — keeps first backup). - Transform compose: strip
env_file:(defensive — books VDS has none), absolutize./paths to/usr/docker/<stack>/. - Parse
.envinto API env array (no-op for books VDS — no.envfiles). - Delete existing Portainer stack with same name on endpoint 1 (idempotency for re-runs).
- Down ad-hoc containers via
docker rm -fbycom.docker.compose.projectlabel (bypasses docker-compose v1ContainerConfigbug). - 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>/dataetc. 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
- Update
.wiki/entities/books-vds.md— flip each stack from "SSH-managed" to "Portainer-managed" table. - Extend or fork
.wiki/concepts/portainer-stack-management-vds.mdwith books VDS section + gotchas if new ones encountered. - Close task as 🟢.