Skip to content

Instantly share code, notes, and snippets.

@tsposato
Created January 14, 2026 23:43
Show Gist options
  • Select an option

  • Save tsposato/67d877228f129a33b4a6cdafed824cfb to your computer and use it in GitHub Desktop.

Select an option

Save tsposato/67d877228f129a33b4a6cdafed824cfb to your computer and use it in GitHub Desktop.
#!/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