homelab-codex-ws/scripts/deploy/deploy-node.sh
oskar 686aca7060 fix(deploy-node): pass --env-file per-service so env-interpolated binds resolve (fleet-prometheus 0.0.0.0 leak)
Without --env-file, docker compose resolved variables from the repo root
(cwd), not from services/<service>/.env where the file actually lives.
This caused ${TAILSCALE_BIND_IP} to expand to empty string, binding
fleet-prometheus on 0.0.0.0:9090 instead of the Tailscale-only IP —
a security hole on the public VPS.

Guard mirrors the existing override-file pattern: only add --env-file
when the file exists, so services without .env continue to work as
before. Flag is injected into COMPOSE_CMD (before the `up` subcommand)
so docker compose sees it as a global option.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 13:41:55 +02:00

114 lines
3.3 KiB
Bash
Executable file

#!/usr/bin/env bash
# deployment-node.sh - To be run on the execution node (SOLARIA, PIHA, VPS)
# This script pulls the latest changes and ensures services are running.
set -e
# Configuration
REPO_PATH="${HOME}/homelab-codex-ws"
RUNTIME_PATH="/opt/homelab"
HOSTNAME=$(hostname | tr '[:lower:]' '[:upper:]')
CURRENT_OS_HOST=$(hostname)
echo "--- Starting Deployment on ${HOSTNAME} ---"
# 1. Update Repository
if [ ! -d "$REPO_PATH" ]; then
echo "Error: Repository not found at $REPO_PATH"
exit 1
fi
cd "$REPO_PATH"
echo "Pulling latest changes..."
git pull
# Resolve host directory: search hosts/*/host.yaml for os_hostname == $(hostname)
HOST_LOGICAL=$(python3 -c "
import yaml, os, sys
current = '${CURRENT_OS_HOST}'
hosts_dir = os.path.join('${REPO_PATH}', 'hosts')
for entry in sorted(os.listdir(hosts_dir)):
host_yaml = os.path.join(hosts_dir, entry, 'host.yaml')
if not os.path.isfile(host_yaml):
continue
try:
with open(host_yaml) as f:
data = yaml.safe_load(f)
if data and data.get('os_hostname') == current:
print(entry)
sys.exit(0)
except Exception:
pass
")
if [ -n "$HOST_LOGICAL" ]; then
HOST_DIR="${REPO_PATH}/hosts/${HOST_LOGICAL}"
else
HOST_LOGICAL_FB="$(echo "$CURRENT_OS_HOST" | tr '[:upper:]' '[:lower:]')"
echo "WARN: no os_hostname match for ${CURRENT_OS_HOST}, falling back to lowercase hostname dir" >&2
HOST_DIR="${REPO_PATH}/hosts/${HOST_LOGICAL_FB}"
fi
if [ ! -d "$HOST_DIR" ]; then
echo "Error: No host directory found for ${CURRENT_OS_HOST} (tried ${HOST_DIR})" >&2
exit 1
fi
# 2. Identify Services
SERVICES=()
if [ -f "${HOST_DIR}/services.txt" ]; then
mapfile -t SERVICES < <(grep -v '^\s*#' "${HOST_DIR}/services.txt" | grep -v '^\s*$')
elif [ -f "${HOST_DIR}/services.yaml" ]; then
SERVICES=($(python3 -c "
import yaml, sys
try:
with open('${HOST_DIR}/services.yaml', 'r') as f:
data = yaml.safe_load(f)
if data and 'services' in data:
if isinstance(data['services'], dict):
print(' '.join(data['services'].keys()))
elif isinstance(data['services'], list):
print(' '.join(data['services']))
except Exception as e:
print(f'Error parsing YAML: {e}', file=sys.stderr)
sys.exit(1)
"))
fi
if [ ${#SERVICES[@]} -eq 0 ]; then
echo "No services found for ${HOSTNAME}. Skipping service deployment."
exit 0
fi
# 3. Deploy Services
for service in "${SERVICES[@]}"; do
echo "Deploying service: ${service}..."
COMPOSE_FILE="${REPO_PATH}/services/${service}/docker-compose.yml"
if [ ! -f "$COMPOSE_FILE" ]; then
echo "Warning: Compose file not found for ${service} at ${COMPOSE_FILE}"
continue
fi
TARGET_DIR="${RUNTIME_PATH}/services/${service}"
mkdir -p "$TARGET_DIR"
OVERRIDE_FILE="${HOST_DIR}/runtime/${service}/docker-compose.override.yml"
COMPOSE_CMD="docker compose -f ${COMPOSE_FILE}"
if [ -f "$OVERRIDE_FILE" ]; then
echo "Using override file for ${service}"
COMPOSE_CMD="${COMPOSE_CMD} -f ${OVERRIDE_FILE}"
fi
ENV_FILE="${REPO_PATH}/services/${service}/.env"
if [ -f "$ENV_FILE" ]; then
COMPOSE_CMD="${COMPOSE_CMD} --env-file ${ENV_FILE}"
fi
$COMPOSE_CMD up -d --remove-orphans
done
echo "--- Deployment Complete ---"