#!/usr/bin/env bash # Validate docker-compose for production/Coolify deploy safety # SPDX-License-Identifier: Apache-2.0 set -euo pipefail REPO="${1:-.}" REPO="$(cd "$REPO" && pwd)" COOLIFY_STRICT="${COOLIFY_STRICT:-0}" ERR=0 WARN=0 err() { echo "ERROR $*"; ERR=$((ERR + 1)); } warn() { echo "WARN $*"; WARN=$((WARN + 1)); } info() { echo "INFO $*"; } pass() { echo "OK $*"; } COMPOSE_FILE="" for f in docker-compose.yml docker-compose.yaml compose.yml; do if [ -f "$REPO/$f" ]; then COMPOSE_FILE="$REPO/$f" break fi done if [ -z "$COMPOSE_FILE" ]; then echo "INFO No docker-compose file — skip" exit 0 fi echo "=== validate-docker-compose: $COMPOSE_FILE ===" # Coolify-only keys (exclude_from_hc) are stripped before docker compose config COMPOSE_VALIDATE_FILE="$COMPOSE_FILE" if grep -q 'exclude_from_hc:' "$COMPOSE_FILE"; then COMPOSE_VALIDATE_FILE="$(mktemp)" sed '/exclude_from_hc:/d' "$COMPOSE_FILE" > "$COMPOSE_VALIDATE_FILE" info "[DC-00] stripped exclude_from_hc for docker compose config" fi # docker compose config if command -v docker >/dev/null 2>&1; then if docker compose -f "$COMPOSE_VALIDATE_FILE" config -q 2>/dev/null; then pass "[DC-01] docker compose config OK" else err "[DC-01] docker compose config failed" docker compose -f "$COMPOSE_VALIDATE_FILE" config 2>&1 | tail -20 || true fi else warn "[DC-01] docker not available — syntax not fully validated" fi [ "$COMPOSE_VALIDATE_FILE" != "$COMPOSE_FILE" ] && rm -f "$COMPOSE_VALIDATE_FILE" # Coolify: no host ports on HTTP services if grep -E '^\s+ports:' "$COMPOSE_FILE" >/dev/null 2>&1; then err "[DC-02] ports: found — Coolify uses SERVICE_URL_* instead" else pass "[DC-02] no host ports (Coolify-safe)" fi # SERVICE_FQDN on frontend with port 8080 (Frappe nginx default; Coolify Traefik target) if grep -q 'SERVICE_FQDN_FRONTEND_8080' "$COMPOSE_FILE"; then pass "[DC-03] SERVICE_FQDN_FRONTEND_8080 present" else err "[DC-03] missing SERVICE_FQDN_FRONTEND_8080 on frontend" fi if grep -q 'SERVICE_FQDN_FRONTEND' "$COMPOSE_FILE"; then pass "[DC-07] SERVICE_FQDN_FRONTEND present (Coolify domain → SITE_NAME)" else warn "[DC-07] missing SERVICE_FQDN_FRONTEND — SITE_NAME will not track Coolify domain" fi if grep -q 'CUSTOM_IMAGE' "$COMPOSE_FILE" && grep -q 'CUSTOM_TAG' "$COMPOSE_FILE"; then pass "[DC-10] CUSTOM_IMAGE / CUSTOM_TAG configured for Jenkins registry" else warn "[DC-10] missing CUSTOM_IMAGE or CUSTOM_TAG in compose" fi # Bind mounts ./scripts on long-running services (heuristic) if grep -E '^\s+-\s+['\''"]?\./scripts' "$COMPOSE_FILE" >/dev/null 2>&1; then if grep -B30 './scripts' "$COMPOSE_FILE" | grep -qE 'restart:\s+(unless-stopped|always)'; then if [ "$COOLIFY_STRICT" = "1" ]; then err "[DC-04] ./scripts bind mount on restarting service — Coolify may miss files" else warn "[DC-04] ./scripts bind mount — prefer inline compose for Coolify daemons" fi else info "[DC-04] ./scripts mount on non-restarting service (may be OK)" fi fi # SERVICE_URL on compose (Coolify magic vars) if grep -qE 'SERVICE_URL_[A-Z0-9_]+' "$COMPOSE_FILE"; then if grep -qE 'networks:' "$COMPOSE_FILE" && ! grep -qE '^\s+default:' "$COMPOSE_FILE"; then warn "[DC-05] SERVICE_URL_* with custom networks — risk Traefik 504; see coolify-docker-compose skill" fi fi # fabric-price-oracle / gateway port hints if grep -q 'fabric-gateway' "$COMPOSE_FILE"; then if grep -A80 'fabric-gateway:' "$COMPOSE_FILE" | grep -qE "['\"]?3000:3000|GATEWAY_PORT"; then pass "[DC-08] fabric-gateway publishes port 3000" else warn "[DC-08] fabric-gateway may not publish host port 3000" fi fi # Retired oracle stub if grep -q 'fabric-oracle:' "$COMPOSE_FILE"; then if grep -A15 'fabric-oracle:' "$COMPOSE_FILE" | grep -qE 'profiles:|legacy|Retired|exit 0'; then pass "[DC-09] fabric-oracle retired/profiled" else warn "[DC-09] fabric-oracle still active — should be legacy stub" fi fi echo "compose errors=$ERR warnings=$WARN" [ "$ERR" -eq 0 ] || exit 1 exit 0