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>
179 lines
7.2 KiB
Bash
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"
|