Created
January 14, 2026 23:43
-
-
Save tsposato/67d877228f129a33b4a6cdafed824cfb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| ############################################### | |
| # CONFIGURATION | |
| ############################################### | |
| PORTAINER_URL="https://portainer-host:40005" | |
| PORTAINER_COMPOSE_DIR="/mnt/data/containers/portainer/compose" | |
| DOCKHAND_STACK_DIR="/mnt/data/containers/dockhand/stacks/Env" | |
| ############################################### | |
| # DRY RUN FLAG | |
| ############################################### | |
| DRYRUN=false | |
| if [[ "$1" == "--dry-run" ]]; then | |
| DRYRUN=true | |
| echo "[DRY RUN] No files will be copied or modified." | |
| echo | |
| fi | |
| ############################################### | |
| # AUTHENTICATION (interactive, no secrets saved) | |
| ############################################### | |
| echo -n "Portainer Username: " | |
| read USERNAME | |
| echo -n "Portainer Password: " | |
| read -s PASSWORD | |
| echo | |
| echo "Requesting JWT token from Portainer..." | |
| JWT=$(curl -s -k \ | |
| -X POST \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"username\": \"$USERNAME\", \"password\": \"$PASSWORD\"}" \ | |
| "$PORTAINER_URL/api/auth" | jq -r '.jwt') | |
| if [[ "$JWT" == "null" || -z "$JWT" ]]; then | |
| echo "ERROR: Failed to authenticate with Portainer." | |
| exit 1 | |
| fi | |
| echo "Authentication successful." | |
| echo | |
| ############################################### | |
| # FETCH STACK MAP FROM PORTAINER API | |
| ############################################### | |
| echo "Fetching stack list from Portainer..." | |
| STACK_MAP=$(curl -s -k \ | |
| -H "Authorization: Bearer $JWT" \ | |
| "$PORTAINER_URL/api/stacks" \ | |
| | jq -r '.[] | "\(.Id) \(.Name)"') | |
| if [[ -z "$STACK_MAP" ]]; then | |
| echo "ERROR: No stacks returned from Portainer API." | |
| exit 1 | |
| fi | |
| echo "Stack map retrieved:" | |
| echo "$STACK_MAP" | |
| echo | |
| ############################################### | |
| # MIGRATION PROCESS | |
| ############################################### | |
| echo "Starting migration to Dockhand..." | |
| echo | |
| while read -r ID NAME; do | |
| [[ -z "$ID" ]] && continue | |
| SRC="$PORTAINER_COMPOSE_DIR/$ID" | |
| DEST="$DOCKHAND_STACK_DIR/$NAME" | |
| if [[ ! -d "$SRC" ]]; then | |
| echo "Skipping $ID ($NAME): no such Portainer stack directory" | |
| continue | |
| fi | |
| # Find latest version directory (v1, v2, v3...) | |
| LATEST_VERSION=$(ls -1 "$SRC" | grep '^v' | sort -V | tail -n 1) | |
| SRC_VER="$SRC/$LATEST_VERSION" | |
| if [[ ! -d "$SRC_VER" ]]; then | |
| echo "Skipping $ID ($NAME): no version directories found" | |
| continue | |
| fi | |
| echo "Stack $ID → $NAME" | |
| echo " Latest version: $LATEST_VERSION" | |
| echo " Source: $SRC_VER" | |
| echo " Destination: $DEST" | |
| if $DRYRUN; then | |
| echo " [DRY RUN] Would create directory: $DEST" | |
| echo " [DRY RUN] Would copy: docker-compose.yml → $DEST/" | |
| echo " [DRY RUN] Would copy: stack.env → $DEST/" | |
| echo | |
| continue | |
| fi | |
| mkdir -p "$DEST" | |
| cp "$SRC_VER/docker-compose.yml" "$DEST/docker-compose.yml" | |
| cp "$SRC_VER/stack.env" "$DEST/stack.env" | |
| echo " Copied successfully." | |
| echo | |
| done <<< "$STACK_MAP" | |
| if $DRYRUN; then | |
| echo "Dry run finished — no changes were made." | |
| else | |
| echo "Migration complete." | |
| echo "Dockhand stacks are now located in: $DOCKHAND_STACK_DIR" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment