235 lines
7.5 KiB
Bash
235 lines
7.5 KiB
Bash
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
ARCHIVE_PATH=""
|
|
DEPLOY_PATH=""
|
|
COMPOSE_FILE=""
|
|
PROJECT_NAME=""
|
|
SERVICE_NAME=""
|
|
NGINX_SOURCE=""
|
|
NGINX_TARGET=""
|
|
NGINX_COMPOSE_FILE=""
|
|
NGINX_PROJECT_NAME=""
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
deploy-remote.sh --archive <path> --deploy-path <path> --compose-file <name> --project-name <name>
|
|
deploy-remote.sh --archive <path> --deploy-path <path> --compose-file <name> --project-name <name> [--service <name>]
|
|
deploy-remote.sh --archive <path> --deploy-path <path> --compose-file <name> --project-name <name> \
|
|
[--service <name>] [--nginx-source <path>] [--nginx-target <path>] \
|
|
[--nginx-compose-file <path>] [--nginx-project-name <name>]
|
|
|
|
This script only updates the main Goodwalk compose project at the specified
|
|
deployment path. It does not touch unrelated Docker projects or global Docker
|
|
state.
|
|
EOF
|
|
}
|
|
|
|
fail() {
|
|
echo "[deploy-remote] ERROR: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--archive)
|
|
ARCHIVE_PATH="${2:-}"
|
|
shift 2
|
|
;;
|
|
--deploy-path)
|
|
DEPLOY_PATH="${2:-}"
|
|
shift 2
|
|
;;
|
|
--compose-file)
|
|
COMPOSE_FILE="${2:-}"
|
|
shift 2
|
|
;;
|
|
--project-name)
|
|
PROJECT_NAME="${2:-}"
|
|
shift 2
|
|
;;
|
|
--service)
|
|
SERVICE_NAME="${2:-}"
|
|
shift 2
|
|
;;
|
|
--nginx-source)
|
|
NGINX_SOURCE="${2:-}"
|
|
shift 2
|
|
;;
|
|
--nginx-target)
|
|
NGINX_TARGET="${2:-}"
|
|
shift 2
|
|
;;
|
|
--nginx-compose-file)
|
|
NGINX_COMPOSE_FILE="${2:-}"
|
|
shift 2
|
|
;;
|
|
--nginx-project-name)
|
|
NGINX_PROJECT_NAME="${2:-}"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
fail "Unknown argument: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ -n "$ARCHIVE_PATH" ]] || fail "--archive is required"
|
|
[[ -n "$DEPLOY_PATH" ]] || fail "--deploy-path is required"
|
|
[[ -n "$COMPOSE_FILE" ]] || fail "--compose-file is required"
|
|
[[ -n "$PROJECT_NAME" ]] || fail "--project-name is required"
|
|
if [[ -n "$SERVICE_NAME" ]]; then
|
|
SERVICE_NAME="$(printf '%s' "$SERVICE_NAME" | xargs)"
|
|
fi
|
|
[[ "$DEPLOY_PATH" != "/" ]] || fail "Refusing to deploy to /"
|
|
[[ -f "$ARCHIVE_PATH" ]] || fail "Archive not found: $ARCHIVE_PATH"
|
|
|
|
nginx_args=("$NGINX_SOURCE" "$NGINX_TARGET" "$NGINX_COMPOSE_FILE" "$NGINX_PROJECT_NAME")
|
|
nginx_args_present=0
|
|
for value in "${nginx_args[@]}"; do
|
|
if [[ -n "$value" ]]; then
|
|
nginx_args_present=1
|
|
break
|
|
fi
|
|
done
|
|
|
|
if (( nginx_args_present )); then
|
|
[[ -n "$NGINX_SOURCE" ]] || fail "--nginx-source is required when nginx deployment is enabled"
|
|
[[ -n "$NGINX_TARGET" ]] || fail "--nginx-target is required when nginx deployment is enabled"
|
|
[[ -n "$NGINX_COMPOSE_FILE" ]] || fail "--nginx-compose-file is required when nginx deployment is enabled"
|
|
[[ -n "$NGINX_PROJECT_NAME" ]] || fail "--nginx-project-name is required when nginx deployment is enabled"
|
|
fi
|
|
|
|
if docker compose version >/dev/null 2>&1; then
|
|
COMPOSE_CMD=(docker compose)
|
|
elif command -v docker-compose >/dev/null 2>&1; then
|
|
COMPOSE_CMD=(docker-compose)
|
|
else
|
|
fail "Docker Compose is not installed on the server"
|
|
fi
|
|
|
|
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/goodwalk-deploy.XXXXXX")"
|
|
|
|
cleanup() {
|
|
rm -rf "$STAGING_DIR"
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
echo "[deploy-remote] Deploying main Goodwalk stack"
|
|
echo "[deploy-remote] Target deployment path: $DEPLOY_PATH"
|
|
echo "[deploy-remote] Compose file: $COMPOSE_FILE"
|
|
echo "[deploy-remote] Docker project: $PROJECT_NAME"
|
|
if [[ -n "$SERVICE_NAME" ]]; then
|
|
echo "[deploy-remote] Target service: $SERVICE_NAME"
|
|
fi
|
|
if (( nginx_args_present )); then
|
|
echo "[deploy-remote] Nginx config source: $NGINX_SOURCE"
|
|
echo "[deploy-remote] Nginx config target: $NGINX_TARGET"
|
|
echo "[deploy-remote] Nginx compose file: $NGINX_COMPOSE_FILE"
|
|
echo "[deploy-remote] Nginx project: $NGINX_PROJECT_NAME"
|
|
fi
|
|
echo "[deploy-remote] Staging archive in: $STAGING_DIR"
|
|
|
|
mkdir -p "$DEPLOY_PATH"
|
|
tar -xzf "$ARCHIVE_PATH" -C "$STAGING_DIR"
|
|
|
|
[[ -f "$STAGING_DIR/$COMPOSE_FILE" ]] || fail "Compose file missing from uploaded archive: $COMPOSE_FILE"
|
|
|
|
if [[ -f "$DEPLOY_PATH/.env" ]]; then
|
|
echo "[deploy-remote] Preserving existing $DEPLOY_PATH/.env"
|
|
fi
|
|
|
|
echo "[deploy-remote] Copying application files into $DEPLOY_PATH"
|
|
if command -v rsync >/dev/null 2>&1; then
|
|
rsync -a \
|
|
--exclude '.env' \
|
|
--exclude '.env.*' \
|
|
"$STAGING_DIR"/ "$DEPLOY_PATH"/
|
|
else
|
|
while IFS= read -r -d '' item; do
|
|
relative_path="${item#"$STAGING_DIR"/}"
|
|
|
|
if [[ "$relative_path" == ".env" || "$relative_path" == .env.* ]]; then
|
|
continue
|
|
fi
|
|
|
|
destination="$DEPLOY_PATH/$relative_path"
|
|
|
|
if [[ -d "$item" ]]; then
|
|
mkdir -p "$destination"
|
|
continue
|
|
fi
|
|
|
|
mkdir -p "$(dirname "$destination")"
|
|
cp -f "$item" "$destination"
|
|
done < <(find "$STAGING_DIR" -mindepth 1 -print0)
|
|
fi
|
|
|
|
[[ -f "$DEPLOY_PATH/$COMPOSE_FILE" ]] || fail "Compose file missing after copy: $DEPLOY_PATH/$COMPOSE_FILE"
|
|
|
|
if [[ ! -f "$DEPLOY_PATH/.env" ]]; then
|
|
if [[ -f "$DEPLOY_PATH/deploy.env.template" ]]; then
|
|
echo "[deploy-remote] No remote .env found. Creating $DEPLOY_PATH/.env from deploy.env.template"
|
|
cp "$DEPLOY_PATH/deploy.env.template" "$DEPLOY_PATH/.env"
|
|
else
|
|
fail "Remote .env is missing and deploy.env.template was not uploaded"
|
|
fi
|
|
fi
|
|
|
|
cd "$DEPLOY_PATH"
|
|
|
|
echo "[deploy-remote] Validating compose configuration"
|
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" config >/dev/null
|
|
|
|
if [[ -n "$SERVICE_NAME" ]]; then
|
|
AVAILABLE_SERVICES="$("${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" config --services)"
|
|
if ! grep -Fxq "$SERVICE_NAME" <<<"$AVAILABLE_SERVICES"; then
|
|
fail "Service '$SERVICE_NAME' was not found in $COMPOSE_FILE. Available services: $(tr '\n' ',' <<<"$AVAILABLE_SERVICES" | sed 's/,$//')"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$SERVICE_NAME" ]]; then
|
|
echo "[deploy-remote] Stopping only the Goodwalk service: $SERVICE_NAME"
|
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" stop "$SERVICE_NAME" || true
|
|
|
|
echo "[deploy-remote] Rebuilding and starting only the Goodwalk service: $SERVICE_NAME"
|
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" up -d --build "$SERVICE_NAME"
|
|
else
|
|
echo "[deploy-remote] Stopping only the Goodwalk project containers"
|
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" stop || true
|
|
|
|
echo "[deploy-remote] Rebuilding and starting only the Goodwalk project containers"
|
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" up -d --build --remove-orphans
|
|
fi
|
|
|
|
echo "[deploy-remote] Current Goodwalk container status"
|
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" ps
|
|
|
|
if [[ -z "$SERVICE_NAME" || "$SERVICE_NAME" == "app" || "$SERVICE_NAME" == "db" ]]; then
|
|
echo "[deploy-remote] Syncing homepage content into PostgreSQL"
|
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" exec -T app node scripts/sync-homepage-content.mjs
|
|
fi
|
|
|
|
if (( nginx_args_present )); then
|
|
[[ -f "$DEPLOY_PATH/$NGINX_SOURCE" ]] || fail "Nginx config missing from deployment payload: $DEPLOY_PATH/$NGINX_SOURCE"
|
|
[[ -f "$NGINX_COMPOSE_FILE" ]] || fail "Nginx compose file was not found on the server: $NGINX_COMPOSE_FILE"
|
|
|
|
echo "[deploy-remote] Updating shared nginx config to avoid stale container IPs"
|
|
mkdir -p "$(dirname "$NGINX_TARGET")"
|
|
cp "$DEPLOY_PATH/$NGINX_SOURCE" "$NGINX_TARGET"
|
|
|
|
echo "[deploy-remote] Validating nginx configuration"
|
|
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -t
|
|
|
|
echo "[deploy-remote] Reloading shared nginx"
|
|
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -s reload
|
|
fi
|
|
|
|
echo "[deploy-remote] Remote deployment finished"
|