Files
project.linkedcorp.net/scripts/install.sh
T
Milton Gonzalez f8e42d4377 feat: instalador para servidor nuevo + fix migracion 006 en BD vacia
scripts/install.sh automatiza el primer despliegue en un servidor Debian/Ubuntu nuevo: verifica
requisitos (Node.js, npm, cliente mysql, systemd, espacio, puertos libres), pregunta dominio,
puertos y base de datos (MariaDB local auto-instalada o un servidor existente), genera
backend/.env con secretos aleatorios, corre migraciones + catalogos, compila el frontend y
crea/arranca los servicios systemd. No crea usuarios: el primer registro publico se vuelve
superadmin solo (bootstrap ya existente).

Probando el instalador de punta a punta contra una base de datos recien creada goo un bug real:
la migracion 006_seed_it_baseline.sql (datos de ejemplo especificos de LinkedCorp) fallaba por
foreign key al no existir todavia catalogos ni usuario. Corregido para que detecte esa situacion
y quede inerte en instalaciones nuevas, sin tocar el comportamiento ya probado en dev/prod.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 15:43:11 -05:00

390 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# Instalador de project.linkedcorp.net para un servidor NUEVO (Debian/Ubuntu, arquetipo B:
# backend + frontend como dos servicios systemd separados).
#
# Uso: cloná el repo en /opt/apps/project.linkedcorp.net/source (ver DEPLOY_WEB.md, paso
# "Clonar el repositorio") y desde ahi corré:
#
# sudo scripts/install.sh
#
# Que hace:
# 1. Verifica requisitos (Node.js, npm, cliente mysql, systemd, espacio, puertos libres).
# 2. Pregunta dominio, puertos, y datos de base de datos: MariaDB local (se instala sola si
# hace falta) o un servidor MySQL/MariaDB existente.
# 3. Crea la base de datos + usuario de la app (clave aleatoria), genera backend/.env con
# todos los secretos generados al azar (JWT_SECRET, ENCRYPTION_KEY).
# 4. Instala dependencias, compila el frontend, corre migraciones + catalogos base.
# 5. Crea y arranca los dos servicios systemd, verifica que respondan.
#
# Que NO hace (queda manual, es especifico de cada servidor/proveedor):
# - Reverse proxy + certificado TLS hacia el dominio publico (ver DEPLOY_WEB.md, paso
# "Configurar Nginx Proxy Manager").
# - Crear un usuario de la app: el primer registro publico en /registro se vuelve
# superadministrador automaticamente (bootstrap ya implementado en el backend) — no hace
# falta ningun paso extra ni script aparte.
set -euo pipefail
APP_NAME="project.linkedcorp.net"
SVC_BACKEND="project-linkedcorp-backend"
SVC_FRONTEND="project-linkedcorp-frontend"
MIN_NODE_MAJOR=20
MIN_FREE_MB=1024
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
C_OK="\033[32m"; C_FAIL="\033[31m"; C_WARN="\033[33m"; C_RESET="\033[0m"
FAILED=0
ok() { echo -e " ${C_OK}OK${C_RESET} $1"; }
fail() { echo -e " ${C_FAIL}FALTA${C_RESET} $1"; FAILED=1; }
warn() { echo -e " ${C_WARN}AVISO${C_RESET} $1"; }
require_root() {
if [[ $EUID -ne 0 ]]; then
echo "Este instalador necesita privilegios de root (instala paquetes, crea servicios systemd)."
echo "Ejecuta: sudo $0"
exit 1
fi
}
# ---------- 1. verificacion de requisitos ----------
check_requirements() {
echo "== Verificando requisitos =="
if command -v node >/dev/null 2>&1; then
local major
major="$(node -v | sed 's/^v//' | cut -d. -f1)"
if [[ "$major" -ge "$MIN_NODE_MAJOR" ]]; then
ok "Node.js $(node -v)"
else
fail "Node.js $(node -v) — se requiere >= v${MIN_NODE_MAJOR}"
fi
else
fail "Node.js no instalado (requiere >= v${MIN_NODE_MAJOR})"
fi
command -v npm >/dev/null 2>&1 && ok "npm $(npm -v)" || fail "npm no instalado"
command -v mysql >/dev/null 2>&1 && ok "cliente mysql" || fail "cliente mysql no instalado (paquete mariadb-client)"
command -v systemctl >/dev/null 2>&1 && ok "systemd" || fail "systemd no disponible (este instalador lo necesita)"
command -v openssl >/dev/null 2>&1 && ok "openssl" || fail "openssl no instalado (necesario para generar secretos)"
command -v curl >/dev/null 2>&1 && ok "curl" || fail "curl no instalado (necesario para verificar al final)"
if command -v apt-get >/dev/null 2>&1; then
ok "apt-get disponible"
else
warn "apt-get no disponible — si eliges 'MariaDB local' vas a tener que instalarla vos antes de continuar"
fi
local avail_kb
avail_kb="$(df -Pk /opt 2>/dev/null | tail -1 | awk '{print $4}')"
if [[ -n "$avail_kb" && "$avail_kb" -ge $((MIN_FREE_MB * 1024)) ]]; then
ok "espacio libre en /opt: $((avail_kb / 1024)) MB"
else
fail "menos de ${MIN_FREE_MB}MB libres en /opt"
fi
if [[ -f "$SOURCE_DIR/backend/package.json" && -f "$SOURCE_DIR/frontend/package.json" ]]; then
ok "repo detectado en $SOURCE_DIR"
else
fail "no se encontro backend/frontend en $SOURCE_DIR — corre este script desde la raiz del repo ya clonado"
fi
echo
if [[ "$FAILED" -ne 0 ]]; then
echo "Faltan requisitos — resuelve lo anterior antes de continuar."
exit 1
fi
echo "Requisitos OK."
echo
}
port_free() {
local port="$1"
! ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[.:]${port}\$"
}
check_ports() {
echo "== Verificando puertos =="
local bad=0
for p in "$BACKEND_PORT" "$FRONTEND_PORT"; do
if port_free "$p"; then
ok "puerto $p libre"
else
echo -e " ${C_FAIL}OCUPADO${C_RESET} puerto $p"
bad=1
fi
done
echo
if [[ "$bad" -ne 0 ]]; then
echo "Elige otro puerto o libera el que esta ocupado, y vuelve a correr el instalador."
exit 1
fi
}
# ---------- 2. configuracion interactiva ----------
ask() {
local prompt="$1" default="$2" var
read -r -p "$prompt [$default]: " var
echo "${var:-$default}"
}
ensure_run_user() {
if id "$RUN_USER" >/dev/null 2>&1; then
ok "usuario '$RUN_USER' ya existe"
else
echo "El usuario '$RUN_USER' no existe — se crea ahora (sistema, sin login)."
useradd --system --create-home --shell /usr/sbin/nologin "$RUN_USER"
ok "usuario '$RUN_USER' creado"
fi
}
gather_config() {
echo "== Configuracion =="
APP_ROOT="$(ask "Directorio de instalacion" "/opt/apps/${APP_NAME}")"
DOMAIN="$(ask "Dominio publico (URL completa, para CORS)" "https://${APP_NAME}")"
BACKEND_PORT="$(ask "Puerto del backend" "4019")"
FRONTEND_PORT="$(ask "Puerto del frontend" "5196")"
RUN_USER="$(ask "Usuario del sistema que corre los servicios" "${SUDO_USER:-deploy}")"
echo
ensure_run_user
echo
check_ports
echo "Base de datos:"
echo " 1) MariaDB local (se instala en este mismo servidor si hace falta)"
echo " 2) Servidor MySQL/MariaDB existente (otra IP)"
local db_choice
db_choice="$(ask "Elige una opcion" "1")"
DB_NAME="$(ask "Nombre de la base de datos" "project_linkedcorp")"
DB_APP_USER="$(ask "Usuario de la app para la BD" "project_linkedcorp")"
if [[ "$db_choice" == "2" ]]; then
DB_LOCAL=0
DB_HOST="$(ask "Host de la base de datos" "")"
DB_PORT="$(ask "Puerto" "3306")"
DB_ADMIN_USER="$(ask "Usuario admin de esa BD (solo para crear la BD/usuario — no se guarda)" "root")"
read -r -s -p "Clave del usuario admin: " DB_ADMIN_PASSWORD; echo
GRANT_HOST="$(ask "IP de este servidor (para el GRANT — desde donde se conecta la app)" "$(hostname -I 2>/dev/null | awk '{print $1}')")"
else
DB_LOCAL=1
DB_HOST="127.0.0.1"
DB_PORT="3306"
GRANT_HOST="127.0.0.1"
fi
echo
}
# ---------- 3. base de datos ----------
setup_local_mariadb() {
if command -v mysql >/dev/null 2>&1 && systemctl is-active --quiet mariadb 2>/dev/null; then
ok "MariaDB ya esta instalada y corriendo"
return
fi
echo "Instalando MariaDB local..."
apt-get update -y
apt-get install -y mariadb-server
systemctl enable --now mariadb
ok "MariaDB instalada y arrancada"
}
mysql_admin() {
# Ejecuta el SQL que llega por stdin como admin. Local: root via socket (sin clave, tipico en
# Debian/Ubuntu con auth_socket). Remoto: con host/usuario/clave provistos.
if [[ "$DB_LOCAL" -eq 1 ]]; then
mysql -u root
else
MYSQL_PWD="$DB_ADMIN_PASSWORD" mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_ADMIN_USER"
fi
}
create_database() {
echo "== Base de datos =="
[[ "$DB_LOCAL" -eq 1 ]] && setup_local_mariadb
DB_APP_PASSWORD="$(openssl rand -base64 18 | tr -d '/+=' | cut -c1-20)"
if ! mysql_admin <<SQL
CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${DB_APP_USER}'@'${GRANT_HOST}' IDENTIFIED BY '${DB_APP_PASSWORD}';
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_APP_USER}'@'${GRANT_HOST}';
FLUSH PRIVILEGES;
SQL
then
echo "No se pudo conectar/crear la base de datos con esas credenciales admin."
exit 1
fi
ok "base '${DB_NAME}' y usuario '${DB_APP_USER}'@'${GRANT_HOST}' listos"
echo
}
# ---------- 4. estructura + .env ----------
setup_tree() {
echo "== Directorios =="
mkdir -p "$APP_ROOT"/{backend,frontend,storage,logs}
chown -R "$RUN_USER":"$RUN_USER" "$APP_ROOT"
ok "$APP_ROOT/{backend,frontend,storage,logs}"
echo
}
write_env() {
echo "== backend/.env =="
local jwt_secret enc_key
jwt_secret="$(openssl rand -hex 32)"
enc_key="$(openssl rand -hex 32)"
cat > "$APP_ROOT/backend/.env" <<EOF
PORT=${BACKEND_PORT}
NODE_ENV=production
DB_HOST=${DB_HOST}
DB_PORT=${DB_PORT}
DB_NAME=${DB_NAME}
DB_USER=${DB_APP_USER}
DB_PASSWORD=${DB_APP_PASSWORD}
JWT_SECRET=${jwt_secret}
JWT_EXPIRES=8h
ENCRYPTION_KEY=${enc_key}
CORS_ORIGIN=${DOMAIN}
STORAGE_ROOT=${APP_ROOT}/storage
EOF
chown "$RUN_USER":"$RUN_USER" "$APP_ROOT/backend/.env"
chmod 640 "$APP_ROOT/backend/.env"
ok "generado (secretos aleatorios, permisos 640)"
echo
}
# ---------- 5. dependencias, migraciones, build ----------
install_backend() {
echo "== Backend =="
# El servicio corre DESDE source/backend (ver write_services) — es donde tienen que vivir
# los node_modules, no en el runtime de $APP_ROOT (que solo guarda el .env persistente).
(cd "$SOURCE_DIR/backend" && npm install --omit=dev)
ok "dependencias instaladas"
echo "Aplicando migraciones y catalogos base..."
(
cd "$SOURCE_DIR/backend"
set -a; source "$APP_ROOT/backend/.env"; set +a
node src/scripts/migrate-db.js
node src/scripts/seed-db.js
)
ok "migraciones y catalogos aplicados"
echo
}
build_frontend() {
echo "== Frontend =="
(cd "$SOURCE_DIR/frontend" && npm install && npm run build)
rm -rf "$APP_ROOT/frontend/dist"
cp -r "$SOURCE_DIR/frontend/dist" "$APP_ROOT/frontend/dist"
cat > "$APP_ROOT/frontend/package.json" <<'EOF'
{ "name": "project-linkedcorp-frontend-runtime", "private": true, "version": "1.0.0",
"dependencies": { "serve": "^14.2.6" } }
EOF
(cd "$APP_ROOT/frontend" && npm install --omit=dev)
chown -R "$RUN_USER":"$RUN_USER" "$APP_ROOT/frontend"
ok "compilado y publicado en $APP_ROOT/frontend"
echo
}
# ---------- 6. systemd ----------
write_services() {
echo "== Servicios systemd =="
cat > "/etc/systemd/system/${SVC_BACKEND}.service" <<EOF
[Unit]
Description=${APP_NAME} backend
After=network.target
[Service]
Type=simple
User=${RUN_USER}
WorkingDirectory=${SOURCE_DIR}/backend
ExecStart=/usr/bin/node src/server.js
EnvironmentFile=${APP_ROOT}/backend/.env
Restart=on-failure
RestartSec=5
StandardOutput=append:${APP_ROOT}/logs/backend.log
StandardError=append:${APP_ROOT}/logs/backend-error.log
[Install]
WantedBy=multi-user.target
EOF
cat > "/etc/systemd/system/${SVC_FRONTEND}.service" <<EOF
[Unit]
Description=${APP_NAME} frontend
After=network.target
[Service]
Type=simple
User=${RUN_USER}
WorkingDirectory=${APP_ROOT}/frontend
ExecStart=/usr/bin/node node_modules/.bin/serve -s dist -l ${FRONTEND_PORT}
Restart=on-failure
RestartSec=5
StandardOutput=append:${APP_ROOT}/logs/frontend.log
StandardError=append:${APP_ROOT}/logs/frontend-error.log
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now "$SVC_BACKEND" "$SVC_FRONTEND"
ok "servicios creados y arrancados"
echo
}
verify() {
echo "== Verificacion =="
sleep 2
if curl -sf "http://127.0.0.1:${BACKEND_PORT}/health" >/dev/null; then
ok "backend responde en :${BACKEND_PORT}"
else
warn "backend no responde todavia — revisa: journalctl -u ${SVC_BACKEND} -n 50"
fi
if curl -sfo /dev/null "http://127.0.0.1:${FRONTEND_PORT}/"; then
ok "frontend responde en :${FRONTEND_PORT}"
else
warn "frontend no responde todavia — revisa: journalctl -u ${SVC_FRONTEND} -n 50"
fi
echo
}
summary() {
echo "======================================================"
echo " Instalacion lista"
echo "======================================================"
echo " Codigo: ${SOURCE_DIR}"
echo " Runtime: ${APP_ROOT}"
echo " Backend: http://127.0.0.1:${BACKEND_PORT} (systemctl status ${SVC_BACKEND})"
echo " Frontend: http://127.0.0.1:${FRONTEND_PORT} (systemctl status ${SVC_FRONTEND})"
echo
echo " Falta (manual, fuera del alcance de este instalador):"
echo " - Reverse proxy + TLS hacia ${DOMAIN} (ver DEPLOY_WEB.md, 'Nginx Proxy Manager'):"
echo " / -> puerto ${FRONTEND_PORT}"
echo " /api -> puerto ${BACKEND_PORT}"
echo " - Entra a ${DOMAIN}/registro y crea el primer usuario: se vuelve"
echo " superadministrador automaticamente, sin pasos extra."
echo " - Configura el correo SMTP desde Configuracion una vez logueado (opcional)."
echo " - Opcional: find ${SOURCE_DIR} -type f -iname '*.md' -delete (limpia la documentacion"
echo " del checkout en el servidor de produccion)."
echo "======================================================"
}
main() {
require_root
check_requirements
gather_config
create_database
setup_tree
write_env
install_backend
build_frontend
write_services
verify
summary
}
main "$@"