' | sudo bash -s -- --token= # # Copy the command from WordPress › NexWordX › Connect Account › Your own server: it carries a # one-time token (two hours) and this script carries your WordPress address. The script: # # 1. installs curl, unzip and Node.js 22 (NodeSource) when Node.js 20 or newer is missing, # and adds 2 GB of swap on servers with less than 2 GB of memory (the build needs it); # 2. asks WordPress for the frontend's settings and downloads the frontend project; # 3. builds it in /opt/nexwordx-frontend and runs it as the "nexwordx" system user # (systemd service "nexwordx-frontend", restarted on failure and at boot); # 4. with a domain: installs Caddy and serves https:// (certificate included; # the domain's DNS must point at this server, ports 80 and 443 open); # without one: the site answers on http://:3000; # 5. tells WordPress where the frontend runs: NexWordX connects to it. # # Run it again with a new command to update the frontend. Remove it with: # systemctl disable --now nexwordx-frontend && rm -rf /opt/nexwordx-frontend /etc/systemd/system/nexwordx-frontend.service set -Eeuo pipefail # One block, so bash reads the whole script before it runs anything: with curl … | bash, a # command that reads standard input could otherwise swallow the rest of the script. { API='__NEXWORDX_API__' TOKEN="" BASE="/opt/nexwordx-frontend" RUN_USER="nexwordx" SERVICE="nexwordx-frontend" NODE_MAJOR="22" for arg in "$@"; do case "$arg" in --token=*) TOKEN="${arg#*=}" ;; *) echo "Unknown option: $arg" >&2; exit 2 ;; esac done say() { printf '\n\033[1;32m==>\033[0m %s\n' "$1"; } note() { printf ' %s\n' "$1"; } # A JSON string for a message (before Node.js is installed: quotes and backslashes dropped). json_string() { if command -v node >/dev/null 2>&1; then node -e 'process.stdout.write(JSON.stringify(process.argv[1]))' "$1" else printf '"%s"' "$(printf '%s' "$1" | tr -d '"\\' | tr '\n' ' ')" fi } # Tell WordPress what is happening: report [running|error]. report() { curl -fsS -m 20 -X POST -H "X-Nexwordx-Install: $TOKEN" -H 'Content-Type: application/json' \ --data "{\"step\":\"$1\",\"message\":$(json_string "$2"),\"state\":\"${3:-running}\"}" \ "$API/progress" >/dev/null 2>&1 || true } fail() { trap - ERR report error "$1" error printf '\n\033[1;31mThe installation stopped:\033[0m %s\n' "$1" >&2 exit 1 } trap 'fail "a command failed on line $LINENO: ${BASH_COMMAND}"' ERR # Read one value of a JSON document: json_value '' 'env.NEXWORDX_SITE_URL'. json_value() { node -e ' let v = JSON.parse(process.argv[1]); for (const k of process.argv[2].split(".")) v = v == null ? undefined : v[k]; process.stdout.write(v == null ? "" : String(v)); ' "$1" "$2" } [ "$(id -u)" -eq 0 ] || { echo "Run the command with sudo (as root)." >&2; exit 1; } command -v apt-get >/dev/null 2>&1 || { echo "This installer works on Ubuntu and Debian servers (apt)." >&2; exit 1; } [[ "$TOKEN" =~ ^[A-Za-z0-9]{40}$ ]] || { echo "Copy the whole command from NexWordX › Connect Account (the token is missing or broken)." >&2; exit 1; } case "$API" in http://* | https://*) ;; *) echo "Download this script from your WordPress site (NexWordX › Connect Account)." >&2; exit 1 ;; esac # ---------------------------------------------------------------- 1. the system say "Checking the server" export DEBIAN_FRONTEND=noninteractive command -v curl >/dev/null 2>&1 || { apt-get update -qq && apt-get install -y -qq curl ca-certificates >/dev/null; } report system "Preparing the server (packages, memory)" apt-get update -qq apt-get install -y -qq ca-certificates curl unzip gnupg >/dev/null memory_mb=$(awk '/MemTotal/ { print int($2 / 1024) }' /proc/meminfo) if [ "${memory_mb:-0}" -lt 1900 ] && ! swapon --show 2>/dev/null | grep -q .; then note "Adding 2 GB of swap (this server has ${memory_mb} MB of memory)." if [ ! -f /swapfile ]; then fallocate -l 2G /swapfile 2>/dev/null || dd if=/dev/zero of=/swapfile bs=1M count=2048 status=none chmod 600 /swapfile mkswap /swapfile >/dev/null fi swapon /swapfile 2>/dev/null || true grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab fi # ---------------------------------------------------------------- Node.js node_major=0 if command -v node >/dev/null 2>&1; then node_major=$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0) fi if [ "$node_major" -lt 20 ]; then say "Installing Node.js ${NODE_MAJOR}" report node "Installing Node.js ${NODE_MAJOR}" curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - >/dev/null apt-get install -y -qq nodejs >/dev/null else note "Node.js $(node -v) is already installed." fi NODE_BIN=$(command -v node) NPM_BIN=$(command -v npm) # ---------------------------------------------------------------- 2. settings and the project say "Fetching the settings and the frontend from WordPress" report download "Downloading the frontend" CONFIG=$(curl -fsS -m 60 -X POST -H "X-Nexwordx-Install: $TOKEN" -H 'Content-Type: application/json' --data '{}' "$API/config") \ || { echo "WordPress did not accept the token: the command expired (two hours) or was used. Create a new one in NexWordX › Connect Account." >&2; exit 1; } DOMAIN=$(json_value "$CONFIG" domain) PORT=$(json_value "$CONFIG" port) SITE_URL=$(json_value "$CONFIG" siteUrl) [[ "$PORT" =~ ^[0-9]{2,5}$ ]] || PORT=3000 [[ -z "$DOMAIN" || "$DOMAIN" =~ ^[a-z0-9.-]+$ ]] || fail "WordPress sent a domain this script cannot use: $DOMAIN" id -u "$RUN_USER" >/dev/null 2>&1 || useradd --system --home-dir "$BASE" --shell /usr/sbin/nologin "$RUN_USER" mkdir -p "$BASE" chown "$RUN_USER":"$RUN_USER" "$BASE" ENV_FILE="$BASE/env.local" umask 077 node -e ' const c = JSON.parse(process.argv[1]); const lines = Object.entries(c.env || {}).map(([k, v]) => k + "=" + String(v).replace(/[\r\n]/g, "")); process.stdout.write(lines.join("\n") + "\n"); ' "$CONFIG" > "$ENV_FILE" umask 022 chown "$RUN_USER":"$RUN_USER" "$ENV_FILE" chmod 600 "$ENV_FILE" WORK=$(mktemp -d) curl -fsS -m 600 -X POST -H "X-Nexwordx-Install: $TOKEN" "$API/package" -o "$WORK/frontend.zip" \ || fail "The frontend could not be downloaded from WordPress." unzip -q "$WORK/frontend.zip" -d "$WORK" [ -f "$WORK/nexwordx-frontend/package.json" ] || fail "The downloaded package is not the NexWordX frontend." rm -rf "$BASE/app.new" mv "$WORK/nexwordx-frontend" "$BASE/app.new" rm -rf "$WORK" cp "$ENV_FILE" "$BASE/app.new/.env.local" chown -R "$RUN_USER":"$RUN_USER" "$BASE/app.new" chmod 600 "$BASE/app.new/.env.local" # ---------------------------------------------------------------- 3. build and run say "Installing packages and building the site (this takes a few minutes)" report build "Installing packages and building the site" as_app() { (cd "$BASE/app.new" && runuser -u "$RUN_USER" -- env HOME="$BASE" NEXT_TELEMETRY_DISABLED=1 "$@"); } as_app "$NPM_BIN" install --no-audit --no-fund --loglevel=error >/dev/null || fail "npm install failed (see the messages above)." as_app "$NPM_BIN" run build || fail "The build failed (see the messages above)." # The standalone server (next.config: output "standalone") with its public files and static assets. [ -f "$BASE/app.new/.next/standalone/server.js" ] || fail "The build did not produce .next/standalone/server.js." cp -r "$BASE/app.new/public" "$BASE/app.new/.next/standalone/" 2>/dev/null || true mkdir -p "$BASE/app.new/.next/standalone/.next" cp -r "$BASE/app.new/.next/static" "$BASE/app.new/.next/standalone/.next/" chown -R "$RUN_USER":"$RUN_USER" "$BASE/app.new/.next/standalone" say "Starting the frontend" report service "Starting the frontend service" if [ -n "$DOMAIN" ]; then LISTEN="127.0.0.1"; else LISTEN="0.0.0.0"; fi systemctl stop "$SERVICE" >/dev/null 2>&1 || true rm -rf "$BASE/app.old" [ -d "$BASE/app" ] && mv "$BASE/app" "$BASE/app.old" mv "$BASE/app.new" "$BASE/app" cat > "/etc/systemd/system/$SERVICE.service" </dev/null 2>&1 up=0 for _ in $(seq 1 60); do if curl -fsS -m 5 "http://127.0.0.1:$PORT/api/nexwordx/health/" >/dev/null 2>&1; then up=1; break; fi sleep 2 done if [ "$up" != 1 ]; then # A reinstall that does not start: the previous build goes back into service. if [ -d "$BASE/app.old" ]; then rm -rf "$BASE/app.failed" mv "$BASE/app" "$BASE/app.failed" mv "$BASE/app.old" "$BASE/app" systemctl restart "$SERVICE" >/dev/null 2>&1 || true fail "The new frontend did not start, so the previous one runs again. The failed build is in $BASE/app.failed. See: journalctl -u $SERVICE -n 80" fi fail "The frontend did not start. See: journalctl -u $SERVICE -n 80" fi rm -rf "$BASE/app.old" "$BASE/app.failed" # ---------------------------------------------------------------- 4. HTTPS (with a domain) if [ -n "$DOMAIN" ]; then say "Serving https://$DOMAIN (Caddy, automatic certificate)" report https "Setting up HTTPS for $DOMAIN" if ! command -v caddy >/dev/null 2>&1; then if command -v ss >/dev/null 2>&1 && ss -ltnH '( sport = :80 or sport = :443 )' 2>/dev/null | grep -q .; then fail "Another web server (nginx or Apache) already uses port 80 or 443. Point $DOMAIN to http://127.0.0.1:$PORT in it, or stop it and run the command again." fi apt-get install -y -qq debian-keyring debian-archive-keyring apt-transport-https >/dev/null 2>&1 || true curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' > /etc/apt/sources.list.d/caddy-stable.list apt-get update -qq apt-get install -y -qq caddy >/dev/null fi mkdir -p /etc/caddy/sites printf '%s {\n\tencode zstd gzip\n\treverse_proxy 127.0.0.1:%s\n}\n' "$DOMAIN" "$PORT" > "/etc/caddy/sites/nexwordx-$DOMAIN.caddy" grep -q 'import sites/\*' /etc/caddy/Caddyfile 2>/dev/null || printf '\nimport sites/*.caddy\n' >> /etc/caddy/Caddyfile if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q 'Status: active'; then ufw allow 80/tcp >/dev/null && ufw allow 443/tcp >/dev/null fi systemctl enable caddy >/dev/null 2>&1 || true systemctl reload caddy >/dev/null 2>&1 || systemctl restart caddy secure=0 for _ in $(seq 1 30); do if curl -fsS -m 8 "https://$DOMAIN/api/nexwordx/health/" >/dev/null 2>&1; then secure=1; break; fi sleep 4 done if [ "$secure" = 0 ]; then note "https://$DOMAIN does not answer yet: check that the domain's DNS (an A record) points at this server" note "and that ports 80 and 443 are open. Caddy keeps trying and gets the certificate once it can." fi else if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q 'Status: active'; then ufw allow "$PORT/tcp" >/dev/null fi fi # ---------------------------------------------------------------- 5. connect say "Connecting WordPress to the frontend" report connect "Connecting WordPress" curl -fsS -m 60 -X POST -H "X-Nexwordx-Install: $TOKEN" -H 'Content-Type: application/json' --data '{}' "$API/done" >/dev/null \ || fail "WordPress did not take the frontend's address. Enter $SITE_URL in NexWordX › Connect Account." trap - ERR printf '\n\033[1;32mDone.\033[0m Your NexWordX site runs at %s\n' "$SITE_URL" printf 'Service: systemctl status %s Logs: journalctl -u %s -f\n' "$SERVICE" "$SERVICE" exit 0 } NEXWORDX_INSTALL;