Files
hope-events/scripts/install.sh
T
joshuaandClaude Opus 5.5 6de0b5525b Rename the product to Cross Events in UI text and docs
Display text only: storage keys, slugs, the encryption salt, the repo URL
and applied migrations keep their old names so existing installs keep working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:06:20 +02:00

179 lines
7.2 KiB
Bash

#!/usr/bin/env bash
# Installs Cross Events on a fresh Ubuntu server (22.04 or 24.04): Node 20, PostgreSQL, PM2,
# nginx and an HTTPS certificate, then the app itself. Finish in the browser at /setup.
#
# Usage (as root, or with sudo):
# sudo bash install.sh --domain events.yourchurch.org --email you@yourchurch.org
#
# Options:
# --domain the site's address (required); DNS must already point at this server
# --email for the HTTPS certificate (Let's Encrypt) notices (required unless --no-ssl)
# --repo git URL (default: https://c-git.crosscode.co.za/joshua/hope-events.git)
# --version tag to install, e.g. v1.41.0 (default: the latest main)
# --dir where to install (default: /opt/hope-events)
# --user Linux user that owns and runs the app (default: hopeevents, created if needed)
# --name PM2 / nginx name, unique per install on a server (default: hope-events)
# --web-port frontend port (default: 3000) --api-port backend port (default: 5000)
# --no-ssl skip the certificate (e.g. behind another proxy that does HTTPS)
#
# Re-running is safe: existing database credentials, .env files and PM2 config are kept.
# Updates after that: `bash scripts/deploy.sh vX.Y.Z` from the install folder.
set -Eeuo pipefail
DOMAIN=""; EMAIL=""; REPO="https://c-git.crosscode.co.za/joshua/hope-events.git"; VERSION="main"
DIR="/opt/hope-events"; APP_USER="hopeevents"; NAME="hope-events"; WEB_PORT=3000; API_PORT=5000; SSL=1
while [ $# -gt 0 ]; do
case "$1" in
--domain) DOMAIN="$2"; shift 2 ;;
--email) EMAIL="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;;
--dir) DIR="$2"; shift 2 ;;
--user) APP_USER="$2"; shift 2 ;;
--name) NAME="$2"; shift 2 ;;
--web-port) WEB_PORT="$2"; shift 2 ;;
--api-port) API_PORT="$2"; shift 2 ;;
--no-ssl) SSL=0; shift ;;
-h|--help) sed -n '2,22p' "$0"; exit 0 ;;
*) echo "Unknown option: $1 (see --help)"; exit 1 ;;
esac
done
fail() { echo "ERROR: $*" >&2; exit 1; }
step() { echo; echo "==> $*"; }
[ "$(id -u)" = 0 ] || fail "Run as root (sudo bash install.sh …)."
[ -n "$DOMAIN" ] || fail "--domain is required."
[ "$SSL" = 0 ] || [ -n "$EMAIL" ] || fail "--email is required for the HTTPS certificate (or use --no-ssl)."
[[ "$NAME" =~ ^[a-z0-9-]+$ ]] || fail "--name may only use a-z, 0-9 and -."
DB_NAME="$(echo "$NAME" | tr '-' '_')"
SCHEME="https"; [ "$SSL" = 1 ] || SCHEME="http"
SITE_URL="$SCHEME://$DOMAIN"
as_app() { sudo -u "$APP_USER" -H bash -lc "$*"; }
random() { openssl rand -base64 48 | tr -d '/+=\n' | head -c "${1:-48}"; }
step "System packages"
export DEBIAN_FRONTEND=noninteractive
apt-get update -q
apt-get install -y -q curl git nginx postgresql postgresql-client openssl build-essential ca-certificates gnupg
[ "$SSL" = 0 ] || apt-get install -y -q certbot python3-certbot-nginx
step "Node.js 20"
if ! command -v node >/dev/null || [ "$(node -p 'process.versions.node.split(".")[0]')" -lt 20 ]; then
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y -q nodejs
fi
node -v
command -v pm2 >/dev/null || npm install -g pm2
step "App user ($APP_USER)"
id "$APP_USER" >/dev/null 2>&1 || useradd --create-home --shell /bin/bash "$APP_USER"
step "Database ($DB_NAME)"
CRED_FILE="/root/.${NAME}-db"
if [ -f "$CRED_FILE" ]; then
DB_PASS="$(cat "$CRED_FILE")"
else
DB_PASS="$(random 32)"
echo "$DB_PASS" > "$CRED_FILE"; chmod 600 "$CRED_FILE"
fi
sudo -u postgres psql -v ON_ERROR_STOP=1 -q <<SQL
DO \$\$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$DB_NAME') THEN
CREATE ROLE "$DB_NAME" LOGIN PASSWORD '$DB_PASS';
ELSE
ALTER ROLE "$DB_NAME" WITH LOGIN PASSWORD '$DB_PASS';
END IF;
END \$\$;
SQL
sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname = '$DB_NAME'" | grep -q 1 \
|| sudo -u postgres createdb -O "$DB_NAME" "$DB_NAME"
DATABASE_URL="postgresql://$DB_NAME:$DB_PASS@localhost:5432/$DB_NAME"
step "Code ($VERSION → $DIR)"
if [ ! -d "$DIR/.git" ]; then
mkdir -p "$DIR"; chown "$APP_USER:$APP_USER" "$DIR"
as_app "git clone --quiet '$REPO' '$DIR'"
fi
as_app "cd '$DIR' && git fetch --tags --quiet origin && if [ '$VERSION' = main ]; then git checkout -q --detach origin/main; else git checkout -q '$VERSION'; fi"
step "Configuration"
if [ ! -f "$DIR/backend/.env" ]; then
cat > "$DIR/backend/.env" <<ENV
NODE_ENV=production
PORT=$API_PORT
DATABASE_URL=$DATABASE_URL
JWT_SECRET=$(random 64)
FRONTEND_URL=$SITE_URL
APP_BASE_URL=$SITE_URL
BACKEND_URL=$SITE_URL
TRUST_PROXY=1
APP_TIMEZONE=Africa/Johannesburg
ENV
echo " wrote backend/.env (keep JWT_SECRET: it also encrypts stored secrets)"
else
echo " backend/.env exists — kept"
fi
if [ ! -f "$DIR/frontend/.env.production" ]; then
echo "NEXT_PUBLIC_API_URL=$SITE_URL" > "$DIR/frontend/.env.production"
fi
if [ ! -f "$DIR/ecosystem.config.js" ]; then
sed -e "s/'hope-events-backend'/'$NAME-backend'/" -e "s/'hope-events-frontend'/'$NAME-frontend'/" -e "s/-p 3000/-p $WEB_PORT/" \
"$DIR/ecosystem.config.example.js" > "$DIR/ecosystem.config.js"
fi
chown "$APP_USER:$APP_USER" "$DIR/backend/.env" "$DIR/frontend/.env.production" "$DIR/ecosystem.config.js"
chmod 600 "$DIR/backend/.env"
step "Install, migrate, build"
as_app "cd '$DIR/backend' && npm ci --no-audit --no-fund && npx prisma migrate deploy && npx prisma generate"
as_app "cd '$DIR/frontend' && npm ci --no-audit --no-fund && npm run build"
step "Start with PM2"
as_app "cd '$DIR' && pm2 startOrReload ecosystem.config.js && pm2 save"
env PATH="$PATH:/usr/bin" pm2 startup systemd -u "$APP_USER" --hp "$(eval echo "~$APP_USER")" >/dev/null
step "nginx ($DOMAIN)"
cat > "/etc/nginx/sites-available/$NAME" <<NGINX
server {
listen 80;
server_name $DOMAIN;
client_max_body_size 25m;
# The API, uploaded files, health and API docs go to the backend; everything else to the site.
location ~ ^/(api|uploads|health|docs)(/|\$) {
proxy_pass http://127.0.0.1:$API_PORT;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
location / {
proxy_pass http://127.0.0.1:$WEB_PORT;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
NGINX
ln -sf "/etc/nginx/sites-available/$NAME" "/etc/nginx/sites-enabled/$NAME"
nginx -t && systemctl reload nginx
if [ "$SSL" = 1 ]; then
step "HTTPS certificate"
certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos -m "$EMAIL" --redirect
fi
step "Checking"
for i in $(seq 1 30); do
if curl -fsS "http://127.0.0.1:$API_PORT/health" >/dev/null 2>&1; then break; fi
sleep 2
done
curl -fsS "http://127.0.0.1:$API_PORT/health" && echo || echo " The backend isn't answering yet — check: sudo -u $APP_USER pm2 logs"
echo
echo "Done. Open $SITE_URL/setup to create the admin account, then enter the licence key."
echo "Database password: $CRED_FILE. Updates: cd $DIR && sudo -u $APP_USER bash scripts/deploy.sh vX.Y.Z"