1. What the Script Does
| Step | Purpose |
|---|---|
| 1. Prompts you for input | Domain, sub‑domain, Git repo, branch/tag, internal port, Let’s Encrypt email, and any environment variables. |
| 2. Installs packages | git, nginx, ufw, certbot, latest Node LTS, and the lightweight serve package for static hosting. |
| 3. Builds your Vite project | npm ci && npm run build inside /opt/<sub>.<domain> |
| 4. Creates/updates a systemd service | Runs serve -s dist -l <PORT> under user www‑data. If the service already exists, it restarts it so changes take effect. |
| 5. Configures Nginx | Sets up a virtual host pointing https://<sub>.<domain> to 127.0.0.1:<PORT> with automatic HTTPS via Certbot. |
| 6. Enables the firewall | Opens only SSH and HTTPS. |
| 7. Prints DNS instructions | Shows the exact A record you need to add. |
Re‑run the script any time you want to:
-
spin up another demo (pick a new sub‑domain + port)
-
redeploy an existing one (same sub‑domain + port, script will restart the service)
2. Prerequisites
-
Ubuntu 22.04 LTS or Debian 12 on your Hetzner VPS (script may work elsewhere but is tested here).
-
A domain you control, with the ability to create A/AAAA records.
-
An SSH key for root login.
3. Quick‑Start
# On your workstation
scp deploy_demo_app.sh root@<SERVER_IP>:/root
ssh root@<SERVER_IP>
# On the server
sudo bash /root/deploy_demo_app.sh
Then follow the interactive prompts.
4. Environment Variables
When the script says “Add an env var (key=value) or press [Enter] to stop” it is building an /etc/<sub>.env file that gets sourced by the systemd service.
4.1 Supabase Example
Copy‑paste these two lines (with your own values) when prompted:
# Supabase Configuration
VITE_SUPABASE_URL=https://<your-project-id>.supabase.co
VITE_SUPABASE_ANON_KEY=<your_anon_key_here>
-
VITE_prefix is required by Vite so the vars are exposed to the browser bundle. -
The env‑file is chmod 640 (readable by
rootandwww-dataonly). -
You can reopen the file later:
sudo nano /etc/<sub>.envand restart:sudo systemctl restart <sub>.
Tip: Need other secrets (Stripe, Sentry, etc.)? Just add them in the same key=value format.
5. Managing a Demo Instance
| Action | Command |
| Check status | sudo systemctl status <sub> |
| View realtime logs | sudo journalctl -u <sub> -f |
| Restart after env‑file change | sudo systemctl restart <sub> |
| Stop / start | sudo systemctl stop <sub> / sudo systemctl start <sub> |
| Remove permanently | sudo systemctl disable --now <sub> then sudo rm /etc/systemd/system/<sub>.service & sudo rm /etc/nginx/sites-enabled/<fqdn>.conf & sudo rm -r /opt/<fqdn> |
6. Troubleshooting
| Symptom | Likely Cause | Fix |
| 502 Bad Gateway | Nginx points to a port where no service is listening. | sudo systemctl restart <sub> (if you redeployed with a new port) or update Nginx proxy_pass. |
| Port in use | Another service already bound to that port. | Choose a different port (or stop the conflicting service). |
| Certbot fails | DNS not propagated yet or port 80 blocked. | Verify A‑record, wait a minute, ensure port 80 is open (UFw allows Nginx Full). |
7. Security Notes
-
Env‑files live in
/etcwith limited permissions; adjust as needed. -
www-datanon‑privileged user runs the apps. -
UFw defaults to deny‑incoming except SSH & Nginx.
8. Updating the Script
The script is idempotent. Replace it with a newer version and rerun — existing demos will either restart (if same sub‑domain) or stay untouched (if different).
COPY PASTE THE FOLLOWING CODE INTO THE ROOT OF THE VPS. EXECUTE IT AND ENTER THE REQUIRED DATA WHEN PROMPTED.
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# Automated Demo-App Deployment Script for a Hetzner VPS
# -----------------------------------------------------------------------------
# Features
# • Deploy any Git-hosted Vite front‑end (perfect for demo builds)
# • Reverse‑proxy via Nginx with per‑sub‑domain TLS (Let’s Encrypt/Certbot)
# • Runs each demo as an isolated systemd service on its own port
# • Prompts you for Supabase (or any) env vars and stores them securely
# • Meant to be **re‑run** for every additional demo you need
# -----------------------------------------------------------------------------
# Usage
# 1. Copy to server → scp deploy_demo_app.sh root@<IP>:/root
# 2. SSH in & run → sudo bash /root/deploy_demo_app.sh
# 3. Follow prompts, then add the suggested DNS record.
# -----------------------------------------------------------------------------
# Tested on: Ubuntu 22.04 LTS, Debian 12 (bookworm) — requires systemd
# -----------------------------------------------------------------------------
set -euo pipefail
# ---------- helper functions ----------
color() { local c="$1"; shift; echo -e "\033[${c}m$*\033[0m"; }
ask() { read -rp "$1: " _ans && echo "${_ans}"; }
check_port() {
if ss -tln | awk '{print $4}' | grep -Eq ":$1$"; then
color 31 "Port $1 appears to be in use. Choose another." && exit 1
fi
}
root_check() {
if [[ $EUID -ne 0 ]]; then
color 31 "Please run as root (use sudo)." && exit 1
fi
}
root_check
# ---------- collect user input ----------
PRIMARY_DOMAIN=$(ask "Primary domain (e.g. example.com)")
SUBDOMAIN=$(ask "Sub‑domain for this demo (e.g. demo1)")
FQDN="${SUBDOMAIN}.${PRIMARY_DOMAIN}"
EMAIL=$(ask "Email for Let's Encrypt expiry notices")
REPO_URL=$(ask "Git repository URL")
BRANCH=$(ask "Git branch or tag (default: main)")
BRANCH=${BRANCH:-main}
PORT=$(ask "Internal port number for the app (e.g. 3001)")
check_port "${PORT}"
color 34 "\n---- Collect environment variables ----"
ENV_FILE="/etc/${SUBDOMAIN}.env"
>"${ENV_FILE}"
while true; do
read -rp "Add an env var (key=value) or press [Enter] to stop: " KV || true
[[ -z "${KV}" ]] && break
echo "${KV}" >>"${ENV_FILE}"
done
chmod 640 "${ENV_FILE}"
# ---------- base packages ----------
color 34 "\nInstalling required packages (git, nginx, node, certbot)..."
apt-get update -qq
apt-get install -y -qq git nginx ufw python3-certbot-nginx curl gnupg ca-certificates > /dev/null
# Node.js (LTS) via NodeSource
if ! command -v node >/dev/null; then
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - &>/dev/null
apt-get install -y -qq nodejs > /dev/null
fi
npm install -g serve &>/dev/null || true
# ---------- clone & build repo ----------
APP_DIR="/opt/${FQDN}"
color 34 "\nCloning ${REPO_URL} → ${APP_DIR}..."
rm -rf "${APP_DIR}"
git clone --quiet --depth 1 --branch "${BRANCH}" "${REPO_URL}" "${APP_DIR}"
cd "${APP_DIR}"
color 34 "Installing dependencies & building Vite project..."
npm ci --silent
npm run build --silent
# ---------- systemd service ----------
SERVICE_NAME="${SUBDOMAIN}"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
cat >"${SERVICE_FILE}" <<SERVICE
[Unit]
Description=${FQDN} demo app
After=network.target
[Service]
Type=simple
EnvironmentFile=${ENV_FILE}
WorkingDirectory=${APP_DIR}
ExecStart=/usr/bin/serve -s dist -l ${PORT}
Restart=always
User=www-data
Group=www-data
[Install]
WantedBy=multi-user.target
SERVICE
# (Re)load the unit and ensure it runs with the new port
systemctl daemon-reload
if systemctl is-active --quiet "${SERVICE_NAME}"; then
systemctl restart "${SERVICE_NAME}"
color 32 "systemd service ${SERVICE_NAME} restarted (now on port ${PORT})."
else
systemctl enable --now "${SERVICE_NAME}"
color 32 "systemd service ${SERVICE_NAME} started (port ${PORT})."
fi