#!/usr/bin/env bash # stack-deploy.sh — Safe Docker Swarm stack deployer # Supports: # - Flat: .yaml (legacy, all existing stacks) # - Folder: / (new, multi-compose stacks like immich) # # Folder layout: # compose-files/ # └── immich/ # ├── immich.yml ← main compose (must match stack name) # ├── hwaccel.transcoding.yml ← extension files, merged in glob order # ├── hwaccel.ml.yml # └── immich.env ← optional, same as flat pattern # # Merge strategy: docker compose ... config | docker stack deploy -c - # All files in the folder matching *.yml or *.yaml are included. # Main file (.yml or .yaml) is always passed FIRST. # Remaining files are sorted and appended. set -euo pipefail STACK="${1:?Usage: stack-deploy.sh }" DIR="/volume1/docker/compose-files" PY="$DIR/deploy/envparse.py" # ── Locate compose file(s) ────────────────────────────────────────────────── FOLDER="$DIR/$STACK" FLAT_YAML="$DIR/${STACK}.yaml" FLAT_YML="$DIR/${STACK}.yml" if [ -d "$FOLDER" ]; then MODE="folder" # Main file must be named .yml or .yaml if [ -f "$FOLDER/${STACK}.yml" ]; then MAIN="$FOLDER/${STACK}.yml" elif [ -f "$FOLDER/${STACK}.yaml" ]; then MAIN="$FOLDER/${STACK}.yaml" else echo "ERROR: No main compose file ($STACK.yml or $STACK.yaml) in $FOLDER"; exit 1 fi # Collect extension files: everything else in the folder, sorted EXTRAS=() while IFS= read -r -d '' f; do [ "$f" = "$MAIN" ] && continue EXTRAS+=("$f") done < <(find "$FOLDER" -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) -print0 | sort -z) ENVFILE="$FOLDER/${STACK}.env" elif [ -f "$FLAT_YAML" ]; then MODE="flat" MAIN="$FLAT_YAML" EXTRAS=() ENVFILE="$DIR/${STACK}.env" elif [ -f "$FLAT_YML" ]; then MODE="flat" MAIN="$FLAT_YML" EXTRAS=() ENVFILE="$DIR/${STACK}.env" else echo "ERROR: No compose file or folder found for stack '$STACK' in $DIR" exit 1 fi echo "==> Deploying stack: $STACK [$MODE]" echo " Main: $MAIN" for f in "${EXTRAS[@]:-}"; do [ -n "$f" ] && echo " Extra: $f"; done # ── Build -f flag list ─────────────────────────────────────────────────────── F_FLAGS=(-f "$MAIN") for f in "${EXTRAS[@]:-}"; do [ -n "$f" ] && F_FLAGS+=(-f "$f") done # ── Load env file if present ───────────────────────────────────────────────── HINT=" Hint: 'secret not found' means Woodpecker hasn't provisioned secrets yet.\n Trigger the pipeline: https://woodpecker.bryanmail.net\n" if [ -f "$ENVFILE" ]; then echo " Env: $ENVFILE" eval "$(python3 "$PY" export "$ENVFILE")" VARS="$(python3 "$PY" vars "$ENVFILE")" echo " Vars: $VARS" if [ "${#F_FLAGS[@]}" -gt 2 ]; then # Folder mode with extras: use docker compose config to merge, then deploy docker compose "${F_FLAGS[@]}" config \ | python3 "$PY" strip \ | envsubst "$VARS" \ | docker stack deploy -c - "$STACK" \ || { printf "\n%b" "$HINT"; exit 1; } else # Single file: envsubst directly (preserves existing behavior exactly) envsubst "$VARS" < "$MAIN" \ | python3 "$PY" strip \ | docker stack deploy -c - "$STACK" \ || { printf "\n%b" "$HINT"; exit 1; } fi else echo " Env: (none)" if [ "${#F_FLAGS[@]}" -gt 2 ]; then docker compose "${F_FLAGS[@]}" config \ | python3 "$PY" strip \ | docker stack deploy -c - "$STACK" \ || { printf "\n%b" "$HINT"; exit 1; } else docker stack deploy -c "$MAIN" "$STACK" \ || { printf "\n%b" "$HINT"; exit 1; } fi fi echo "==> Done: $STACK"