Files
compose-files/deploy/stack-deploy.sh
2026-07-08 21:29:15 -07:00

144 lines
4.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# stack-deploy.sh — Safe Docker Swarm stack deployer
# Supports:
# - Flat: <stack>.yaml (legacy, all existing stacks)
# - Folder: <stack>/ (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
#
# Env layering:
# deploy/global.env ← always loaded first (DOMAIN, DOMAIN_NAME, ALT_DOMAIN, etc.)
# <stack>.env ← loaded second; stack values override globals
#
# Merge strategy: docker compose ... config | docker stack deploy -c -
# All files in the folder matching *.yml or *.yaml are included.
# Main file (<stack>.yml or <stack>.yaml) is always passed FIRST.
# Remaining files are sorted and appended.
set -euo pipefail
STACK="${1:?Usage: stack-deploy.sh <stack-name>}"
DIR="/volume1/docker/compose-files"
PY="$DIR/deploy/envparse.py"
GLOBAL_ENV="$DIR/deploy/global.env"
# ── 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 <stack>.yml or <stack>.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 (global base + optional stack override) ─────────────────────────
HINT=" Hint: 'secret not found' means Woodpecker hasn't provisioned secrets yet.\n Trigger the pipeline: https://woodpecker.bryanmail.net\n"
GLOBAL_EXISTS=0
STACK_EXISTS=0
[ -f "$GLOBAL_ENV" ] && GLOBAL_EXISTS=1
[ -f "$ENVFILE" ] && STACK_EXISTS=1
if [ "$GLOBAL_EXISTS" -eq 1 ] && [ "$STACK_EXISTS" -eq 1 ]; then
echo " Env: $GLOBAL_ENV + $ENVFILE (stack overrides global)"
eval "$(python3 "$PY" export_merged "$GLOBAL_ENV" "$ENVFILE")"
VARS="$(python3 "$PY" vars_merged "$GLOBAL_ENV" "$ENVFILE")"
elif [ "$GLOBAL_EXISTS" -eq 1 ]; then
echo " Env: $GLOBAL_ENV (no stack env)"
eval "$(python3 "$PY" export "$GLOBAL_ENV")"
VARS="$(python3 "$PY" vars "$GLOBAL_ENV")"
elif [ "$STACK_EXISTS" -eq 1 ]; then
echo " Env: $ENVFILE (no global env)"
eval "$(python3 "$PY" export "$ENVFILE")"
VARS="$(python3 "$PY" vars "$ENVFILE")"
else
echo " Env: (none)"
VARS=""
fi
echo " Vars: $VARS"
# ── Deploy ───────────────────────────────────────────────────────────────────
if [ -n "$VARS" ]; then
if [ "${#F_FLAGS[@]}" -gt 2 ]; then
# Folder mode with extras: merge via docker compose config
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 "$VARS" < "$MAIN" \
| python3 "$PY" strip \
| docker stack deploy -c - "$STACK" \
|| { printf "\n%b" "$HINT"; exit 1; }
fi
else
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"