mirror of
https://github.com/tiennm99/composes.git
synced 2026-10-11 12:09:25 +00:00
feat: add migrate-service skill for moving Coolify deployments onto this repo
This commit is contained in:
1 parent
dd7c951143
commit
f544979a91
3 files changed
+206
-1
No files matched your search
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: migrate-service
|
||||
description: Move a running Coolify deployment — typically a one-click Service template or a hand-made app — onto the app built from a service directory in this repo, keeping its data. Finds both resources through the Coolify MCP, copies named volumes with docker, and walks env, domain, watch path, deploy and verification. Use when the user wants to migrate, move or switch an old deployment to the composes-repo version. Not for first-time deploys with no data, nor for debugging a service (debug-service).
|
||||
---
|
||||
|
||||
# Migrate a service onto the composes repo
|
||||
|
||||
Carry one service's data and settings from an existing Coolify resource (the
|
||||
**old** one) to the Coolify app built from `<service>/` in this repo (the
|
||||
**new** one), so the new app resumes exactly where the old one stopped. The
|
||||
old resource stays intact until the user deletes it; it is the rollback.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Never read Coolify's stored env files or any `.env` holding real values, and
|
||||
never print secrets. The Coolify MCP shows env key names only; values are
|
||||
copied by the user in the Coolify UI.
|
||||
- Ask before `control` stop/start and before `deploy`. Volume copies overwrite
|
||||
the new resource's data: show the dry run and get confirmation before
|
||||
`--apply`.
|
||||
- Delete nothing of the old resource. Deleting it, its volumes, or the backup
|
||||
volume happens only when the user asks, after verification.
|
||||
|
||||
## 1. Identify both resources
|
||||
|
||||
Read `<service>/compose.yml` and `<service>/README.md`. Then
|
||||
`search_resources` with the service name on every Coolify MCP server
|
||||
(`miti-jp`, `miti-sg`). The old one is usually a Service (`get_service`,
|
||||
`list_service_applications`, `list_service_databases`); the new one is an
|
||||
Application whose `git_repository` is this repo and `base_directory` is
|
||||
`/<service>`. If the new app does not exist, the user creates it first —
|
||||
it must have deployed once so its volumes exist.
|
||||
|
||||
For each, collect: uuid, server, status, domain (`fqdn` on the old service's
|
||||
application, `docker_compose_domains` on the new app), `watch_paths`,
|
||||
`list_storages`, `list_env_keys`.
|
||||
|
||||
## 2. Compare layouts before copying
|
||||
|
||||
Build a table: old volume and mount path → new volume and mount path. Volumes
|
||||
are named `<uuid>_<compose-volume-name>`. Check, against the new compose file:
|
||||
|
||||
- Paths the old deployment used that the new layout moves (a config file or
|
||||
skills dir that was outside a volume, or in a separate volume the new compose
|
||||
folds into another). Inspect contents read-only to see where things really
|
||||
live:
|
||||
`docker run --rm -v <vol>:/v:ro alpine sh -c 'du -sh /v; ls -la /v'`.
|
||||
- Database images: a raw copy of a data directory needs the same engine major
|
||||
version (e.g. `pgvector:pg18` → `pg18`). On a mismatch, stop and dump/restore
|
||||
instead.
|
||||
- Env keys: list keys present on the old resource and missing or differing on
|
||||
the new. Flag the keys that must carry the **same value**: encryption keys
|
||||
(stored data becomes unreadable otherwise) and database user, password and
|
||||
name (they live inside the copied data directory).
|
||||
|
||||
## 3. Stop both resources
|
||||
|
||||
Confirm with the user, then stop the old and the new resource (`control`
|
||||
action `stop`, `confirm: true`). The copy refuses to run while any container
|
||||
mounts a volume it touches.
|
||||
|
||||
## 4. Copy volumes
|
||||
|
||||
The volumes must be on the Docker host this workspace talks to; check with
|
||||
`docker volume ls | grep -E '<old-uuid>|<new-uuid>'`. If they are not, the
|
||||
service runs on another server: give the user the commands to run in that
|
||||
server's Coolify terminal instead.
|
||||
|
||||
```bash
|
||||
scripts/migrate-volumes.sh --from <old-uuid> --to <new-uuid> [--map old=new ...]
|
||||
scripts/migrate-volumes.sh --from <old-uuid> --to <new-uuid> [--map old=new ...] --apply
|
||||
```
|
||||
|
||||
Dry run first; show its output. It pairs volumes by name, skips empty source
|
||||
volumes, and lists unmatched ones — use `--map` when a name differs, or note
|
||||
the volume as intentionally dropped. With `--apply`, each target volume is
|
||||
archived into the volume `migrate-backup-<new-uuid>`, emptied, then filled
|
||||
from the source; it prints `OK` when entry counts match, `DIFF` otherwise.
|
||||
A file that must land in a subdirectory of another volume is a one-off
|
||||
`docker run ... cp -a` after the script, shown to the user first.
|
||||
|
||||
Mount only Docker volumes or host paths in `docker run -v`. This workspace is
|
||||
itself a container: a workspace path given to `-v` resolves on the host, not
|
||||
here, so backups go into a volume.
|
||||
|
||||
## 5. Settings in the Coolify UI (user)
|
||||
|
||||
Give the user one checklist:
|
||||
|
||||
- Env values to copy from old to new, with the must-match keys from step 2
|
||||
marked. Any optional variable the old one used that the new compose keeps
|
||||
commented out needs uncommenting in `<service>/compose.yml`.
|
||||
- Domain: remove it from the old resource, then set it on the new app's
|
||||
container, keeping the port suffix (`https://host:PORT`).
|
||||
- Watch path on the new app: `<service>/**`.
|
||||
|
||||
## 6. Deploy and verify
|
||||
|
||||
Once the user confirms the settings, `deploy` the new app (ask first). Then
|
||||
check the deployment finished (`list_deployments`, `get_deployment` with
|
||||
`include_log_summary`), the status is healthy, `get_logs` shows no auth,
|
||||
decrypt or migration errors, and the domain answers. Ask the user to confirm
|
||||
their data is there in the app itself.
|
||||
|
||||
## 7. Wrap up
|
||||
|
||||
Report what moved and where the backup volume is. Leave the old resource
|
||||
stopped. When the user is satisfied, they may ask to delete the old resource
|
||||
(in Coolify, with its volumes) and `migrate-backup-<new-uuid>`.
|
||||
|
||||
If the service's README describes setup in a way the migration proved wrong,
|
||||
update it.
|
||||
|
||||
## Resources
|
||||
|
||||
- `scripts/migrate-volumes.sh` — pairs, backs up and copies named volumes
|
||||
between two Coolify resources; dry run by default.
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copies the named volumes of an old Coolify resource into the matching volumes
|
||||
# of the new one. Volumes match by the name after the "<uuid>_" prefix. Dry run
|
||||
# by default.
|
||||
#
|
||||
# Usage: migrate-volumes.sh --from <old-uuid> --to <new-uuid> [--map old=new ...] [--apply]
|
||||
# --map pairs volumes whose names differ, e.g. --map db-data=postgres-data.
|
||||
# With --apply, each target volume is first archived into the volume
|
||||
# migrate-backup-<new-uuid>, then emptied and filled with the source contents.
|
||||
set -euo pipefail
|
||||
|
||||
FROM=""
|
||||
TO=""
|
||||
APPLY=0
|
||||
declare -A MAP=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--from) FROM="$2"; shift 2 ;;
|
||||
--to) TO="$2"; shift 2 ;;
|
||||
--map) MAP["${2%%=*}"]="${2#*=}"; shift 2 ;;
|
||||
--apply) APPLY=1; shift ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$FROM" ] && [ -n "$TO" ] || { echo "--from and --to are required" >&2; exit 2; }
|
||||
[[ "$FROM$TO" =~ ^[a-z0-9]+$ ]] || { echo "uuids must be lowercase alphanumeric" >&2; exit 2; }
|
||||
|
||||
IMG=alpine
|
||||
vols() { docker volume ls --format '{{.Name}}' | sed -n "s/^$1_//p" | sort; }
|
||||
stats() { docker run --rm -v "$1":/v:ro "$IMG" sh -c 'echo "$(du -sk /v | cut -f1)K $(find /v | wc -l) entries"'; }
|
||||
in_use() { docker ps --filter "volume=$1" --format '{{.Names}}'; }
|
||||
|
||||
OLD=$(vols "$FROM")
|
||||
NEW=$(vols "$TO")
|
||||
[ -n "$OLD" ] || { echo "no volumes named ${FROM}_* on this Docker host" >&2; exit 1; }
|
||||
[ -n "$NEW" ] || { echo "no volumes named ${TO}_* on this Docker host; deploy the new resource once first" >&2; exit 1; }
|
||||
|
||||
# Source suffix -> target suffix.
|
||||
declare -A PAIRS=()
|
||||
echo "=== VOLUME PAIRS ==="
|
||||
for o in $OLD; do
|
||||
n="${MAP[$o]:-$o}"
|
||||
if [ -z "$(docker run --rm -v "${FROM}_$o":/v:ro "$IMG" find /v -mindepth 1 -maxdepth 1)" ]; then
|
||||
echo "EMPTY ${FROM}_$o (nothing to copy)"
|
||||
elif ! grep -qx "$n" <<<"$NEW"; then
|
||||
echo "UNMATCHED ${FROM}_$o ($(stats "${FROM}_$o")); pass --map $o=<target> if its data is needed"
|
||||
else
|
||||
PAIRS[$o]="$n"
|
||||
echo "COPY ${FROM}_$o ($(stats "${FROM}_$o")) -> ${TO}_$n ($(stats "${TO}_$n"))"
|
||||
fi
|
||||
done
|
||||
for n in $NEW; do
|
||||
matched=0
|
||||
for o in "${!PAIRS[@]}"; do [ "${PAIRS[$o]}" = "$n" ] && matched=1; done
|
||||
[ $matched = 1 ] || echo "UNTOUCHED ${TO}_$n"
|
||||
done
|
||||
|
||||
busy=""
|
||||
for o in "${!PAIRS[@]}"; do
|
||||
for v in "${FROM}_$o" "${TO}_${PAIRS[$o]}"; do
|
||||
c=$(in_use "$v"); [ -z "$c" ] || busy+=" $v used by: $c"$'\n'
|
||||
done
|
||||
done
|
||||
if [ -n "$busy" ]; then
|
||||
printf 'Containers still mount these volumes; stop both resources first:\n%s' "$busy" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ $APPLY = 0 ]; then
|
||||
echo "${#PAIRS[@]} volume(s) would be overwritten. Re-run with --apply to execute."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BACKUP="migrate-backup-$TO"
|
||||
STAMP=$(date +%Y%m%d-%H%M%S)
|
||||
docker volume create "$BACKUP" >/dev/null
|
||||
echo "=== APPLYING (backups in volume $BACKUP) ==="
|
||||
for o in "${!PAIRS[@]}"; do
|
||||
src="${FROM}_$o"; dst="${TO}_${PAIRS[$o]}"
|
||||
docker run --rm -v "$dst":/v:ro -v "$BACKUP":/b "$IMG" tar czf "/b/$STAMP-${PAIRS[$o]}.tgz" -C /v .
|
||||
docker run --rm -v "$src":/from:ro -v "$dst":/to "$IMG" \
|
||||
sh -c 'find /to -mindepth 1 -delete && cp -a /from/. /to/'
|
||||
s=$(stats "$src"); d=$(stats "$dst")
|
||||
if [ "${s#* }" = "${d#* }" ]; then echo " OK $src -> $dst ($d)"
|
||||
else echo " DIFF $src ($s) -> $dst ($d)"; fi
|
||||
done
|
||||
@@ -52,7 +52,9 @@ The `debug-service` skill in `.claude/skills/` walks through the process.
|
||||
## Usage
|
||||
|
||||
In Coolify or Dokploy, point a Docker Compose resource at the service directory
|
||||
and set the environment variables from its `.env.example`.
|
||||
and set the environment variables from its `.env.example`. To move an existing
|
||||
deployment and its data onto that resource, use the `migrate-service` skill in
|
||||
`.claude/skills/`.
|
||||
|
||||
Locally:
|
||||
|
||||
|
||||
Reference in new issue
Block a user