750b7abcd5
- .claude/CLAUDE.md - .claude/agents/fn-recopilador/SKILL.md - .claude/rules/INDEX.md - .claude/rules/cpp_apps.md - bash/functions/infra/build_cpp_windows.sh - cpp/CMakeLists.txt - cpp/PATTERNS.md - cpp/framework/app_base.cpp - cpp/framework/app_base.h - dev/issues/README.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
686 lines
24 KiB
Bash
686 lines
24 KiB
Bash
#!/usr/bin/env bash
|
|
# dockerize_app — Empaqueta una app del registry para deploy a VPS organic-machine
|
|
# via Docker + Traefik + Coolify. Genera Dockerfile, docker-compose.yml,
|
|
# traefik-dynamic.yml, sube via rsync y arranca el stack remoto.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../../" && pwd)"
|
|
|
|
source "$SCRIPT_DIR/../infra/rsync_deploy.sh"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _dockerize_app_usage — imprime ayuda y sale con error
|
|
# ---------------------------------------------------------------------------
|
|
_dockerize_app_usage() {
|
|
cat >&2 <<'USAGE'
|
|
Uso: dockerize_app <app_name> [opciones]
|
|
|
|
--domain DOMAIN Dominio público (ej: kanban.organic-machine.com)
|
|
--port PORT Puerto interno del contenedor (default: 8080)
|
|
--ssh-host HOST Host SSH destino (default: organic-machine.com)
|
|
--remote-dir DIR Directorio remoto (default: /home/ubuntu/coolify-apps/<app>)
|
|
--basic-auth USER:PASS Credenciales basicAuth para Traefik
|
|
--no-auth Deshabilitar basicAuth (por defecto: auth ON)
|
|
--no-gzip Deshabilitar gzip middleware
|
|
--env KEY=VAL Variable de entorno (repetible)
|
|
--volume NAME Volume Docker (se monta en /data)
|
|
--build-cmd CMD Comando de build personalizado
|
|
--standalone Crear repo Gitea + git clone remoto en vez de rsync
|
|
--dry-run Mostrar artefactos generados sin ejecutar nada
|
|
USAGE
|
|
return 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _dockerize_app_generate_dockerfile — genera Dockerfile multi-stage para Go
|
|
# ---------------------------------------------------------------------------
|
|
_dockerize_app_generate_dockerfile() {
|
|
local binary_name="$1"
|
|
local port="$2"
|
|
shift 2
|
|
local env_vars=("$@")
|
|
|
|
cat <<DOCKERFILE
|
|
# Stage build
|
|
FROM golang:1.23-alpine AS builder
|
|
|
|
WORKDIR /app
|
|
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
COPY . .
|
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o ${binary_name} .
|
|
|
|
# Stage final
|
|
FROM alpine:latest
|
|
|
|
RUN apk --no-cache add ca-certificates tzdata
|
|
|
|
WORKDIR /app
|
|
|
|
COPY --from=builder /app/${binary_name} .
|
|
|
|
DOCKERFILE
|
|
|
|
if [[ ${#env_vars[@]} -gt 0 ]]; then
|
|
for kv in "${env_vars[@]}"; do
|
|
[[ -n "$kv" ]] && echo "ENV ${kv%%=*}=${kv#*=}"
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
cat <<DOCKERFILE
|
|
EXPOSE ${port}
|
|
|
|
ENTRYPOINT ["./${binary_name}"]
|
|
DOCKERFILE
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _dockerize_app_generate_compose — genera docker-compose.yml
|
|
# ---------------------------------------------------------------------------
|
|
_dockerize_app_generate_compose() {
|
|
local project_name="$1"
|
|
local service_name="$2"
|
|
local build_context="$3"
|
|
local dockerfile_path="$4"
|
|
local port="$5"
|
|
local volume_name="$6"
|
|
local network="$7"
|
|
shift 7
|
|
local env_vars=("$@")
|
|
|
|
cat <<COMPOSE
|
|
name: ${project_name}
|
|
|
|
services:
|
|
${service_name}:
|
|
build:
|
|
context: ${build_context}
|
|
dockerfile: ${dockerfile_path}
|
|
container_name: ${service_name}
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${port}:${port}"
|
|
COMPOSE
|
|
|
|
if [[ -n "$volume_name" ]]; then
|
|
cat <<COMPOSE
|
|
volumes:
|
|
- ${volume_name}:/data
|
|
COMPOSE
|
|
fi
|
|
|
|
if [[ ${#env_vars[@]} -gt 0 ]]; then
|
|
echo " environment:"
|
|
for kv in "${env_vars[@]}"; do
|
|
local key="${kv%%=*}"
|
|
echo " - ${key}=\${${key}:-}"
|
|
done
|
|
fi
|
|
|
|
cat <<COMPOSE
|
|
networks:
|
|
- ${network}
|
|
COMPOSE
|
|
|
|
if [[ -n "$volume_name" ]]; then
|
|
cat <<COMPOSE
|
|
|
|
volumes:
|
|
${volume_name}:
|
|
COMPOSE
|
|
fi
|
|
|
|
cat <<COMPOSE
|
|
|
|
networks:
|
|
${network}:
|
|
external: true
|
|
COMPOSE
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _dockerize_app_generate_traefik_dynamic — genera traefik-dynamic.yml
|
|
# ---------------------------------------------------------------------------
|
|
_dockerize_app_generate_traefik_dynamic() {
|
|
local name="$1"
|
|
local domain="$2"
|
|
local upstream_url="$3"
|
|
local basic_auth_line="$4" # "" para deshabilitar
|
|
local enable_gzip="$5" # "true" | "false"
|
|
|
|
# Construir lista de middlewares HTTPS
|
|
local https_middlewares=()
|
|
if [[ -n "$basic_auth_line" ]]; then
|
|
https_middlewares+=("${name}-auth")
|
|
fi
|
|
if [[ "$enable_gzip" == "true" ]]; then
|
|
https_middlewares+=("${name}-gzip")
|
|
fi
|
|
|
|
cat <<TRAEFIK
|
|
http:
|
|
routers:
|
|
${name}-http:
|
|
rule: "Host(\`${domain}\`)"
|
|
entryPoints:
|
|
- "http"
|
|
middlewares:
|
|
- "${name}-redirect"
|
|
service: "${name}-service"
|
|
|
|
${name}-https:
|
|
rule: "Host(\`${domain}\`)"
|
|
entryPoints:
|
|
- "https"
|
|
TRAEFIK
|
|
|
|
if [[ ${#https_middlewares[@]} -gt 0 ]]; then
|
|
echo " middlewares:"
|
|
for mw in "${https_middlewares[@]}"; do
|
|
echo " - \"${mw}\""
|
|
done
|
|
fi
|
|
|
|
cat <<TRAEFIK
|
|
service: "${name}-service"
|
|
tls:
|
|
certResolver: letsencrypt
|
|
|
|
services:
|
|
${name}-service:
|
|
loadBalancer:
|
|
servers:
|
|
- url: "${upstream_url}"
|
|
|
|
middlewares:
|
|
${name}-redirect:
|
|
redirectScheme:
|
|
scheme: "https"
|
|
TRAEFIK
|
|
|
|
if [[ -n "$basic_auth_line" ]]; then
|
|
cat <<TRAEFIK
|
|
${name}-auth:
|
|
basicAuth:
|
|
users:
|
|
- "${basic_auth_line}"
|
|
TRAEFIK
|
|
fi
|
|
|
|
if [[ "$enable_gzip" == "true" ]]; then
|
|
cat <<TRAEFIK
|
|
${name}-gzip:
|
|
compress: true
|
|
TRAEFIK
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _dockerize_app_merge_env_file — merge no destructivo de .env
|
|
# Agrega solo keys que no existen ya. Avisa de conflictos.
|
|
# ---------------------------------------------------------------------------
|
|
_dockerize_app_merge_env_file() {
|
|
local env_file="$1"
|
|
shift
|
|
local new_vars=("$@")
|
|
|
|
for kv in "${new_vars[@]}"; do
|
|
local key="${kv%%=*}"
|
|
local val="${kv#*=}"
|
|
if grep -q "^${key}=" "$env_file" 2>/dev/null; then
|
|
echo " WARNING: .env ya contiene '${key}', no se sobreescribe." >&2
|
|
else
|
|
echo "${key}=${val}" >> "$env_file"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# dockerize_app — punto de entrada principal
|
|
# ---------------------------------------------------------------------------
|
|
dockerize_app() {
|
|
local app_name="${1:-}"
|
|
if [[ -z "$app_name" ]]; then
|
|
_dockerize_app_usage
|
|
fi
|
|
shift
|
|
|
|
# Defaults
|
|
local domain=""
|
|
local port=8080
|
|
local ssh_host="organic-machine.com"
|
|
local remote_dir=""
|
|
local basic_auth=""
|
|
local no_auth=false
|
|
local no_gzip=false
|
|
local env_vars=()
|
|
local volume_name=""
|
|
local build_cmd=""
|
|
local standalone=false
|
|
local dry_run=false
|
|
|
|
# Parse args
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--domain) domain="$2"; shift 2 ;;
|
|
--port) port="$2"; shift 2 ;;
|
|
--ssh-host) ssh_host="$2"; shift 2 ;;
|
|
--remote-dir) remote_dir="$2"; shift 2 ;;
|
|
--basic-auth) basic_auth="$2"; shift 2 ;;
|
|
--no-auth) no_auth=true; shift ;;
|
|
--no-gzip) no_gzip=true; shift ;;
|
|
--env) env_vars+=("$2"); shift 2 ;;
|
|
--volume) volume_name="$2"; shift 2 ;;
|
|
--build-cmd) build_cmd="$2"; shift 2 ;;
|
|
--standalone) standalone=true; shift ;;
|
|
--dry-run) dry_run=true; shift ;;
|
|
*) echo "dockerize_app: opcion desconocida '$1'" >&2; _dockerize_app_usage ;;
|
|
esac
|
|
done
|
|
|
|
# Validar que el dominio fue dado
|
|
if [[ -z "$domain" ]]; then
|
|
echo "dockerize_app: --domain es obligatorio" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Auth logic
|
|
local auth_enabled=false
|
|
if [[ "$no_auth" == "false" ]]; then
|
|
auth_enabled=true
|
|
if [[ -z "$basic_auth" ]]; then
|
|
echo "dockerize_app: --basic-auth USER:PASS es obligatorio cuando auth esta ON. Usa --no-auth para deshabilitarlo." >&2
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
local enable_gzip="true"
|
|
if [[ "$no_gzip" == "true" ]]; then
|
|
enable_gzip="false"
|
|
fi
|
|
|
|
local start_ts
|
|
start_ts=$(date +%s)
|
|
|
|
echo "==> [1/13] Validando app '${app_name}' en registry.db..." >&2
|
|
|
|
# Buscar registry.db
|
|
local registry_db="${FN_REGISTRY_ROOT:-$REGISTRY_ROOT}/registry.db"
|
|
if [[ ! -f "$registry_db" ]]; then
|
|
echo "dockerize_app: registry.db no encontrado en '$registry_db'" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Consultar la app en la BD
|
|
local app_row
|
|
app_row=$(sqlite3 "$registry_db" \
|
|
"SELECT dir_path || '|' || lang || '|' || name FROM apps WHERE id LIKE '${app_name}%' OR name = '${app_name}' LIMIT 1;" 2>/dev/null || true)
|
|
if [[ -z "$app_row" ]]; then
|
|
echo "dockerize_app: app '${app_name}' no encontrada en registry.db" >&2
|
|
echo " Consulta apps disponibles: sqlite3 '$registry_db' \"SELECT id, name, lang FROM apps ORDER BY name;\"" >&2
|
|
return 1
|
|
fi
|
|
|
|
local app_dir_rel="${app_row%%|*}"
|
|
local rest="${app_row#*|}"
|
|
local app_lang="${rest%%|*}"
|
|
local app_real_name="${rest#*|}"
|
|
|
|
local app_dir="${REGISTRY_ROOT}/${app_dir_rel}"
|
|
|
|
echo " OK: '${app_real_name}' (lang=${app_lang}) en '${app_dir_rel}'" >&2
|
|
|
|
# Fase 1: solo Go
|
|
if [[ "$app_lang" != "go" ]]; then
|
|
echo "dockerize_app: Phase 1 soporta solo apps Go. Lang detectado: '${app_lang}'" >&2
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -d "$app_dir" ]]; then
|
|
echo "dockerize_app: directorio de la app no encontrado: '$app_dir'" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Default remote_dir
|
|
if [[ -z "$remote_dir" ]]; then
|
|
remote_dir="/home/ubuntu/coolify-apps/${app_real_name}"
|
|
fi
|
|
|
|
echo "==> [2/13] Validando conectividad SSH a '${ssh_host}'..." >&2
|
|
if [[ "$dry_run" == "false" ]]; then
|
|
if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$ssh_host" true 2>/dev/null; then
|
|
echo "dockerize_app: no se puede conectar a '${ssh_host}' via SSH" >&2
|
|
return 1
|
|
fi
|
|
echo " OK: SSH conectado." >&2
|
|
else
|
|
echo " [dry-run] Saltando verificacion SSH." >&2
|
|
fi
|
|
|
|
# Generar bcrypt si auth ON
|
|
local basic_auth_line=""
|
|
local basic_auth_user=""
|
|
if [[ "$auth_enabled" == "true" ]]; then
|
|
echo "==> [3/13] Generando hash bcrypt para basicAuth..." >&2
|
|
basic_auth_user="${basic_auth%%:*}"
|
|
local basic_auth_pass="${basic_auth#*:}"
|
|
if [[ -z "$basic_auth_user" || -z "$basic_auth_pass" ]]; then
|
|
echo "dockerize_app: --basic-auth debe tener formato USER:PASS" >&2
|
|
return 1
|
|
fi
|
|
if command -v htpasswd &>/dev/null; then
|
|
basic_auth_line=$(htpasswd -nbB "$basic_auth_user" "$basic_auth_pass" 2>/dev/null)
|
|
elif command -v python3 &>/dev/null; then
|
|
# Fallback: bcrypt via python3 si está disponible
|
|
basic_auth_line=$(python3 -c "
|
|
import bcrypt, sys
|
|
user, pw = sys.argv[1], sys.argv[2]
|
|
h = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
|
|
print(f'{user}:{h.decode()}')
|
|
" "$basic_auth_user" "$basic_auth_pass" 2>/dev/null) || {
|
|
echo "dockerize_app: ni htpasswd ni python3+bcrypt disponibles para generar hash" >&2
|
|
return 1
|
|
}
|
|
else
|
|
echo "dockerize_app: 'htpasswd' no encontrado. Instalar con: sudo apt install apache2-utils" >&2
|
|
return 1
|
|
fi
|
|
if [[ -z "$basic_auth_line" ]]; then
|
|
echo "dockerize_app: no se pudo generar hash bcrypt" >&2
|
|
return 1
|
|
fi
|
|
echo " OK: hash generado para usuario '${basic_auth_user}'." >&2
|
|
else
|
|
echo "==> [3/13] BasicAuth deshabilitado (--no-auth)." >&2
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Generar Dockerfile
|
|
# -----------------------------------------------------------------------
|
|
echo "==> [4/13] Generando Dockerfile..." >&2
|
|
local dockerfile_path="${app_dir}/Dockerfile"
|
|
local dockerfile_content
|
|
if [[ ${#env_vars[@]} -gt 0 ]]; then
|
|
dockerfile_content=$(_dockerize_app_generate_dockerfile \
|
|
"$app_real_name" \
|
|
"$port" \
|
|
"${env_vars[@]}" 2>/dev/null || true)
|
|
else
|
|
dockerfile_content=$(_dockerize_app_generate_dockerfile \
|
|
"$app_real_name" \
|
|
"$port" 2>/dev/null || true)
|
|
fi
|
|
|
|
if [[ "$dry_run" == "true" ]]; then
|
|
echo "--- Dockerfile (${dockerfile_path}) ---" >&2
|
|
echo "$dockerfile_content" >&2
|
|
echo "---" >&2
|
|
elif [[ -f "$dockerfile_path" ]]; then
|
|
echo " INFO: Dockerfile ya existe en '${dockerfile_path}', no se sobreescribe." >&2
|
|
else
|
|
echo "$dockerfile_content" > "$dockerfile_path"
|
|
echo " OK: Dockerfile generado en '${dockerfile_path}'." >&2
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Generar docker-compose.yml
|
|
# -----------------------------------------------------------------------
|
|
echo "==> [5/13] Generando docker-compose.yml..." >&2
|
|
local compose_path="${app_dir}/docker-compose.yml"
|
|
|
|
# Build context relativo al remote_dir (contexto remoto)
|
|
# Localmente el compose vive en apps/<app>/, el contexto del build Docker
|
|
# apunta al root del repo (../../) para que el Dockerfile pueda hacer COPY . .
|
|
local build_context="../../"
|
|
local dockerfile_in_compose="${app_dir_rel}/Dockerfile"
|
|
|
|
local compose_content
|
|
if [[ ${#env_vars[@]} -gt 0 ]]; then
|
|
compose_content=$(_dockerize_app_generate_compose \
|
|
"$app_real_name" \
|
|
"$app_real_name" \
|
|
"$build_context" \
|
|
"$dockerfile_in_compose" \
|
|
"$port" \
|
|
"$volume_name" \
|
|
"coolify" \
|
|
"${env_vars[@]}" 2>/dev/null || true)
|
|
else
|
|
compose_content=$(_dockerize_app_generate_compose \
|
|
"$app_real_name" \
|
|
"$app_real_name" \
|
|
"$build_context" \
|
|
"$dockerfile_in_compose" \
|
|
"$port" \
|
|
"$volume_name" \
|
|
"coolify" 2>/dev/null || true)
|
|
fi
|
|
|
|
if [[ "$dry_run" == "true" ]]; then
|
|
echo "--- docker-compose.yml (${compose_path}) ---" >&2
|
|
echo "$compose_content" >&2
|
|
echo "---" >&2
|
|
else
|
|
echo "$compose_content" > "$compose_path"
|
|
echo " OK: docker-compose.yml generado en '${compose_path}'." >&2
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Generar traefik-dynamic.yml
|
|
# -----------------------------------------------------------------------
|
|
echo "==> [6/13] Generando traefik-dynamic.yml..." >&2
|
|
local traefik_path="${app_dir}/traefik-dynamic.yml"
|
|
local upstream_url="http://${app_real_name}:${port}"
|
|
# Nombre del router Traefik: reemplazar _ por - para nombres válidos
|
|
local traefik_name="${app_real_name//_/-}"
|
|
|
|
local traefik_content
|
|
traefik_content=$(_dockerize_app_generate_traefik_dynamic \
|
|
"$traefik_name" \
|
|
"$domain" \
|
|
"$upstream_url" \
|
|
"$basic_auth_line" \
|
|
"$enable_gzip" 2>/dev/null || true)
|
|
|
|
if [[ "$dry_run" == "true" ]]; then
|
|
echo "--- traefik-dynamic.yml (${traefik_path}) ---" >&2
|
|
echo "$traefik_content" >&2
|
|
echo "---" >&2
|
|
else
|
|
echo "$traefik_content" > "$traefik_path"
|
|
echo " OK: traefik-dynamic.yml generado en '${traefik_path}'." >&2
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Generar .env
|
|
# -----------------------------------------------------------------------
|
|
local env_path="${app_dir}/.env"
|
|
if [[ ${#env_vars[@]} -gt 0 ]]; then
|
|
echo "==> [7/13] Generando/actualizando .env..." >&2
|
|
if [[ "$dry_run" == "true" ]]; then
|
|
echo "--- .env (${env_path}) ---" >&2
|
|
for kv in "${env_vars[@]}"; do echo "$kv"; done >&2
|
|
echo "---" >&2
|
|
elif [[ -f "$env_path" ]]; then
|
|
echo " INFO: .env ya existe, aplicando merge no destructivo." >&2
|
|
_dockerize_app_merge_env_file "$env_path" "${env_vars[@]}"
|
|
else
|
|
for kv in "${env_vars[@]}"; do echo "$kv"; done > "$env_path"
|
|
echo " OK: .env creado en '${env_path}'." >&2
|
|
fi
|
|
else
|
|
echo "==> [7/13] Sin vars --env, omitiendo .env." >&2
|
|
fi
|
|
|
|
if [[ "$dry_run" == "true" ]]; then
|
|
local end_ts
|
|
end_ts=$(date +%s)
|
|
local duration=$(( end_ts - start_ts ))
|
|
echo "" >&2
|
|
echo "[dry-run] Artefactos generados. Sin cambios remotos." >&2
|
|
printf '{"status":"dry-run","app":"%s","domain":"%s","remote_dir":"%s","port":%d,"auth_enabled":%s,"gzip_enabled":%s,"duration_seconds":%d,"url":"https://%s"}\n' \
|
|
"$app_real_name" "$domain" "$remote_dir" "$port" \
|
|
"$auth_enabled" "$enable_gzip" "$duration" "$domain"
|
|
return 0
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Standalone: crear repo Gitea + push
|
|
# -----------------------------------------------------------------------
|
|
if [[ "$standalone" == "true" ]]; then
|
|
echo "==> [8/13] Creando repo Gitea (standalone mode)..." >&2
|
|
source "$SCRIPT_DIR/../infra/gitea_create_repo.sh"
|
|
source "$SCRIPT_DIR/../infra/gitea_push_directory.sh"
|
|
gitea_create_repo "dataforge" "$app_real_name" "true" "App ${app_real_name} — dockerized via dockerize_app" >&2 || true
|
|
gitea_push_directory "$app_dir" "dataforge" "$app_real_name" "master" >&2
|
|
echo " OK: repo Gitea 'dataforge/${app_real_name}' sincronizado." >&2
|
|
|
|
echo "==> [9/13] Clonando/actualizando repo en remoto (standalone mode)..." >&2
|
|
local gitea_base="${GITEA_URL:-https://git.organic-machine.com}"
|
|
ssh "$ssh_host" bash <<REMOTE
|
|
set -euo pipefail
|
|
if [[ -d '${remote_dir}/.git' ]]; then
|
|
echo " git pull en '${remote_dir}'..."
|
|
cd '${remote_dir}' && git pull origin master
|
|
else
|
|
echo " git clone en '${remote_dir}'..."
|
|
mkdir -p '${remote_dir}'
|
|
git clone '${gitea_base}/dataforge/${app_real_name}.git' '${remote_dir}'
|
|
fi
|
|
REMOTE
|
|
echo " OK: repo remoto actualizado." >&2
|
|
else
|
|
echo "==> [8/13] Rsync del app al VPS..." >&2
|
|
# Sincronizar solo el directorio de la app (más liviano que el repo completo)
|
|
# El build context remoto apunta a ../../ desde remote_dir, por lo que
|
|
# también necesitamos los ficheros Go del root del repo.
|
|
# Solución: rsync el root del registry al VPS en un dir de build, y además
|
|
# sincronizar los artefactos Docker generados.
|
|
local remote_build_root="/home/ubuntu/coolify-build/${app_real_name}"
|
|
echo " Sincronizando repo completo a '${ssh_host}:${remote_build_root}'..." >&2
|
|
rsync_deploy "${REGISTRY_ROOT}/" "$ssh_host" "$remote_build_root" > /dev/null
|
|
echo " OK: repo sincronizado en '${remote_build_root}'." >&2
|
|
|
|
echo "==> [9/13] Preparando directorio remoto de deploy..." >&2
|
|
ssh "$ssh_host" "mkdir -p '${remote_dir}'"
|
|
echo " OK: '${remote_dir}' disponible." >&2
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Subir traefik-dynamic.yml al proxy de Coolify
|
|
# -----------------------------------------------------------------------
|
|
echo "==> [10/13] Subiendo traefik-dynamic.yml a Coolify proxy..." >&2
|
|
local traefik_coolify_path="/data/coolify/proxy/dynamic/${traefik_name}.yml"
|
|
ssh "$ssh_host" "sudo mkdir -p /data/coolify/proxy/dynamic/" >&2
|
|
echo "$traefik_content" | ssh "$ssh_host" \
|
|
"sudo tee '${traefik_coolify_path}' > /dev/null"
|
|
echo " OK: traefik-dynamic.yml en '${traefik_coolify_path}'." >&2
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Subir docker-compose.yml, Dockerfile y .env al remote_dir
|
|
# -----------------------------------------------------------------------
|
|
echo "==> [11/13] Subiendo artefactos Docker a '${ssh_host}:${remote_dir}'..." >&2
|
|
if [[ "$standalone" == "true" ]]; then
|
|
# En standalone, el repo ya está clonado en remote_dir
|
|
echo " [standalone] Artefactos ya en remote_dir via git." >&2
|
|
else
|
|
# Copiar los artefactos generados al remote_dir (que es el subdir de la app en el build root)
|
|
local remote_app_subdir="${remote_build_root}/${app_dir_rel}"
|
|
ssh "$ssh_host" "mkdir -p '${remote_app_subdir}'" >&2
|
|
scp "$compose_path" "${ssh_host}:${remote_app_subdir}/docker-compose.yml" >&2
|
|
scp "$dockerfile_path" "${ssh_host}:${remote_app_subdir}/Dockerfile" >&2
|
|
if [[ -f "$env_path" ]]; then
|
|
scp "$env_path" "${ssh_host}:${remote_app_subdir}/.env" >&2
|
|
fi
|
|
# Apuntar remote_dir al subdir donde está el compose
|
|
remote_dir="$remote_app_subdir"
|
|
echo " OK: artefactos subidos a '${remote_app_subdir}'." >&2
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Verificar red coolify + docker compose up --build
|
|
# -----------------------------------------------------------------------
|
|
echo "==> [12/13] Verificando red Docker 'coolify' y levantando stack..." >&2
|
|
ssh "$ssh_host" bash <<REMOTE
|
|
set -euo pipefail
|
|
if ! docker network ls --format '{{.Name}}' | grep -q '^coolify$'; then
|
|
echo " Creando red Docker 'coolify'..."
|
|
docker network create coolify
|
|
fi
|
|
echo " Red 'coolify' disponible."
|
|
cd '${remote_dir}'
|
|
echo " docker compose build + up..."
|
|
docker compose up -d --build
|
|
echo " Stack levantado."
|
|
REMOTE
|
|
echo " OK: stack Docker activo." >&2
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Health check
|
|
# -----------------------------------------------------------------------
|
|
echo "==> [13/13] Health check en 'https://${domain}'..." >&2
|
|
local health_url="https://${domain}/"
|
|
local attempts=0
|
|
local max_attempts=10
|
|
local http_code="000"
|
|
while [[ $attempts -lt $max_attempts ]]; do
|
|
if [[ -n "$basic_auth_user" ]]; then
|
|
http_code=$(curl -sk -o /dev/null -w "%{http_code}" \
|
|
-u "${basic_auth_user}:${basic_auth#*:}" \
|
|
"$health_url" 2>/dev/null || echo "000")
|
|
else
|
|
http_code=$(curl -sk -o /dev/null -w "%{http_code}" \
|
|
"$health_url" 2>/dev/null || echo "000")
|
|
fi
|
|
# 200 = OK, 401 = basicAuth activo (correcto), 301/302 = redirect (transitorio)
|
|
if [[ "$http_code" == "200" || "$http_code" == "401" ]]; then
|
|
break
|
|
fi
|
|
attempts=$(( attempts + 1 ))
|
|
echo " Intento ${attempts}/${max_attempts} — HTTP ${http_code}, esperando 3s..." >&2
|
|
sleep 3
|
|
done
|
|
|
|
local end_ts
|
|
end_ts=$(date +%s)
|
|
local duration=$(( end_ts - start_ts ))
|
|
|
|
# Obtener container_id
|
|
local container_id=""
|
|
container_id=$(ssh "$ssh_host" \
|
|
"docker ps --filter name=${app_real_name} --format '{{.ID}}' | head -1" 2>/dev/null || true)
|
|
|
|
if [[ "$http_code" == "200" || "$http_code" == "401" ]]; then
|
|
echo " OK: servicio respondiendo HTTP ${http_code}." >&2
|
|
local status_val="ok"
|
|
else
|
|
echo " ERROR: health check timeout tras ${max_attempts} intentos." >&2
|
|
local status_val="failed"
|
|
fi
|
|
|
|
printf '{"status":"%s","app":"%s","domain":"%s","remote_dir":"%s","container_id":"%s","duration_seconds":%d,"auth_enabled":%s,"gzip_enabled":%s,"http_code":"%s","url":"https://%s"}\n' \
|
|
"$status_val" \
|
|
"$app_real_name" \
|
|
"$domain" \
|
|
"$remote_dir" \
|
|
"$container_id" \
|
|
"$duration" \
|
|
"$auth_enabled" \
|
|
"$enable_gzip" \
|
|
"$http_code" \
|
|
"$domain"
|
|
|
|
[[ "$status_val" == "ok" ]]
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Punto de entrada directo (bash dockerize_app.sh <app> [flags])
|
|
# ---------------------------------------------------------------------------
|
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
dockerize_app "$@"
|
|
fi
|