#!/usr/bin/env bash # install.sh — idempotent first-time setup for CurrenciCombo on a systemd host. # # Intended to run ONCE per host as root (or with sudo). Running it again is # safe: it will skip already-present artifacts and warn on conflicts. # # What this does: # 1. Creates the `currencicombo` system user and /opt/currencicombo tree. # 2. Installs nginx (Debian/Ubuntu or Alpine) if not present. # 3. Ensures a local Postgres is running and creates a fresh # `currencicombo` role + DB (refuses to touch an existing one unless # --force-recreate is passed). # 4. Ensures a local Redis is running. # 5. Writes /etc/currencicombo/orchestrator.env from .env.prod.example, # auto-populating EVENT_SIGNING_SECRET and ORCHESTRATOR_API_KEYS with # fresh randoms the first time. # 6. Installs /etc/currencicombo/webapp-nginx.conf. # 7. Installs the two systemd units and runs `systemctl daemon-reload`. # 8. Enables (does NOT start) both units. First start happens via # scripts/deployment/deploy-currencicombo-8604.sh after the first # successful build. # # This script is target-agnostic. It has no hardcoded IP / hostname / # VLAN. The NPMplus ingress in front of it is configured separately — # see scripts/deployment/README.md. set -euo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" APP_USER="currencicombo" APP_HOME="/opt/currencicombo" ETC_DIR="/etc/currencicombo" LOG_DIR="/var/log/currencicombo" REPO_DIR="/var/lib/currencicombo/repo" ENV_FILE="${ETC_DIR}/orchestrator.env" NGINX_FILE="${ETC_DIR}/webapp-nginx.conf" SYSTEMD_DIR="/etc/systemd/system" FORCE_RECREATE_DB=0 DRY_RUN=0 SKIP_NGINX_INSTALL=0 log() { printf '[install] %s\n' "$*" >&2; } warn() { printf '[install][WARN] %s\n' "$*" >&2; } die() { printf '[install][FATAL] %s\n' "$*" >&2; exit 1; } run() { if [[ "${DRY_RUN}" -eq 1 ]]; then printf '[install][dry-run] %s\n' "$*" >&2; else eval "$*"; fi; } usage() { cat <<'USAGE' Usage: sudo ./install.sh [--force-recreate-db] [--skip-nginx-install] [--dry-run] --force-recreate-db DROP and recreate the currencicombo Postgres role and DB even if they already exist. DESTRUCTIVE. --skip-nginx-install Do not apt/apk install nginx (use if you already have a custom nginx build in place). --dry-run Print the commands that would run, don't run them. USAGE } while [[ $# -gt 0 ]]; do case "$1" in --force-recreate-db) FORCE_RECREATE_DB=1; shift ;; --skip-nginx-install) SKIP_NGINX_INSTALL=1; shift ;; --dry-run) DRY_RUN=1; shift ;; -h|--help) usage; exit 0 ;; *) die "unknown arg: $1" ;; esac done [[ "$EUID" -eq 0 ]] || die "must run as root (sudo)" # ---------------------------------------------------------------------- # 1. User + tree # ---------------------------------------------------------------------- if id "${APP_USER}" >/dev/null 2>&1; then log "user ${APP_USER} already exists" else log "creating system user ${APP_USER}" run useradd --system --home-dir "${APP_HOME}" --shell /usr/sbin/nologin --user-group "${APP_USER}" fi for d in "${APP_HOME}" "${APP_HOME}/orchestrator" "${APP_HOME}/webapp" \ "${APP_HOME}/webapp/dist" "${ETC_DIR}" "${LOG_DIR}" "${REPO_DIR}"; do run install -d -o "${APP_USER}" -g "${APP_USER}" -m 0755 "$d" done run chown "${APP_USER}:${APP_USER}" "${APP_HOME}" "${LOG_DIR}" "${REPO_DIR}" run chmod 0750 "${ETC_DIR}" # ---------------------------------------------------------------------- # 2. nginx (required by currencicombo-webapp.service) # ---------------------------------------------------------------------- if [[ "${SKIP_NGINX_INSTALL}" -eq 0 ]]; then if command -v nginx >/dev/null 2>&1; then log "nginx already installed ($(nginx -v 2>&1 | head -1))" elif command -v apt-get >/dev/null 2>&1; then log "installing nginx via apt" run 'DEBIAN_FRONTEND=noninteractive apt-get update -q' run 'DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends nginx-light' # We use our own nginx.conf via -c, so disable the distro site. run systemctl disable --now nginx 2>/dev/null || true elif command -v apk >/dev/null 2>&1; then log "installing nginx via apk" run apk add --no-cache nginx run rc-update del nginx 2>/dev/null || true else die "no apt or apk available — install nginx manually or re-run with --skip-nginx-install" fi fi [[ -f /etc/nginx/mime.types ]] || warn "/etc/nginx/mime.types missing; webapp-nginx.conf may fail" # ---------------------------------------------------------------------- # 3. Postgres role + DB # ---------------------------------------------------------------------- if ! command -v psql >/dev/null 2>&1; then die "psql not on PATH — install Postgres on this host (e.g. apt install postgresql) before running install.sh" fi # Use the OS `postgres` superuser for DDL. pg_role_exists() { sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${APP_USER}';" 2>/dev/null | grep -q 1 } pg_db_exists() { sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${APP_USER}';" 2>/dev/null | grep -q 1 } if pg_role_exists; then if [[ "${FORCE_RECREATE_DB}" -eq 1 ]]; then log "dropping existing role/DB (--force-recreate-db)" run "sudo -u postgres psql -c 'DROP DATABASE IF EXISTS ${APP_USER};'" run "sudo -u postgres psql -c 'DROP ROLE IF EXISTS ${APP_USER};'" else warn "Postgres role ${APP_USER} already exists — skipping role/DB creation. Re-run with --force-recreate-db to wipe." fi fi if ! pg_role_exists; then log "creating Postgres role ${APP_USER}" run "sudo -u postgres psql -c \"CREATE ROLE ${APP_USER} LOGIN;\"" fi if ! pg_db_exists; then log "creating Postgres database ${APP_USER}" run "sudo -u postgres psql -c \"CREATE DATABASE ${APP_USER} OWNER ${APP_USER};\"" fi # Peer auth from the currencicombo OS user → currencicombo DB role "just works" # on Debian-style pg_hba (local all all peer). No password needed. # ---------------------------------------------------------------------- # 4. Redis # ---------------------------------------------------------------------- if systemctl list-unit-files | grep -q '^redis-server\.service'; then run systemctl enable --now redis-server elif systemctl list-unit-files | grep -q '^redis\.service'; then run systemctl enable --now redis elif command -v redis-cli >/dev/null 2>&1; then warn "redis-cli present but no redis-server.service / redis.service unit — assuming external Redis" else warn "redis not detected; orchestrator will fall back to in-process event bus. Install redis for multi-replica support." fi # ---------------------------------------------------------------------- # 5. orchestrator.env # ---------------------------------------------------------------------- FIRST_KEYS_FILE="/root/currencicombo-first-keys.txt" if [[ -f "${ENV_FILE}" ]]; then log "${ENV_FILE} already exists — leaving alone (no new keys generated)" else log "writing ${ENV_FILE}" install -o "${APP_USER}" -g "${APP_USER}" -m 0640 "${SCRIPT_DIR}/.env.prod.example" "${ENV_FILE}" # Auto-fill the two REQUIRED secrets so first boot doesn't crash. SECRET="$(openssl rand -hex 32)" INIT_KEY="$(openssl rand -hex 24)" SETT_KEY="$(openssl rand -hex 24)" AUD_KEY="$(openssl rand -hex 24)" run "sed -i 's|^EVENT_SIGNING_SECRET=.*|EVENT_SIGNING_SECRET=${SECRET}|' '${ENV_FILE}'" run "sed -i 's|^ORCHESTRATOR_API_KEYS=.*|ORCHESTRATOR_API_KEYS=${INIT_KEY}:initiator,${SETT_KEY}:settler,${AUD_KEY}:auditor|' '${ENV_FILE}'" # Write a root-only handoff file so ops can grab the keys without # scraping journald or reading the env file. The canonical copy lives # in ${ENV_FILE}; delete this file once the keys are in your password # manager. if [[ "${DRY_RUN}" -eq 0 ]]; then umask 077 cat > "${FIRST_KEYS_FILE}" <