| [b5bae24] | 1 | # PrutCMS v10 — Deployment Guide
|
|---|
| 2 |
|
|---|
| 3 | End-to-end production install on a fresh Ubuntu 22.04 / Debian 12 VPS
|
|---|
| 4 | (TransIP, Hetzner, etc.). Assumes a non-root user with sudo.
|
|---|
| 5 |
|
|---|
| 6 | ---
|
|---|
| 7 |
|
|---|
| 8 | ## 0. Prerequisites
|
|---|
| 9 |
|
|---|
| 10 | - VPS with Ubuntu 22.04 LTS or Debian 12.
|
|---|
| 11 | - DNS A/AAAA records for your domain pointing at the server's IP. Wait for
|
|---|
| 12 | propagation (`dig +short YOUR-DOMAIN` should return the right IP) before
|
|---|
| 13 | running certbot.
|
|---|
| 14 | - SSH access as a non-root user (e.g. `robin`).
|
|---|
| 15 |
|
|---|
| 16 | ---
|
|---|
| 17 |
|
|---|
| 18 | ## 1. System packages
|
|---|
| 19 |
|
|---|
| 20 | ```bash
|
|---|
| 21 | sudo apt update && sudo apt upgrade -y
|
|---|
| 22 | sudo apt install -y curl ca-certificates git nginx ufw sqlite3
|
|---|
| 23 | ```
|
|---|
| 24 |
|
|---|
| 25 | Install Node.js 20 LTS via NodeSource (don't use the OS-default — it's old):
|
|---|
| 26 |
|
|---|
| 27 | ```bash
|
|---|
| 28 | curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
|---|
| 29 | sudo apt install -y nodejs
|
|---|
| 30 | node --version # should print v20.x.x
|
|---|
| 31 | ```
|
|---|
| 32 |
|
|---|
| 33 | Install PM2 globally:
|
|---|
| 34 |
|
|---|
| 35 | ```bash
|
|---|
| 36 | sudo npm install -g pm2
|
|---|
| 37 | ```
|
|---|
| 38 |
|
|---|
| 39 | ---
|
|---|
| 40 |
|
|---|
| 41 | ## 2. Firewall
|
|---|
| 42 |
|
|---|
| 43 | ```bash
|
|---|
| 44 | sudo ufw allow OpenSSH
|
|---|
| 45 | sudo ufw allow 'Nginx Full' # opens 80 + 443
|
|---|
| 46 | sudo ufw enable
|
|---|
| 47 | ```
|
|---|
| 48 |
|
|---|
| 49 | ---
|
|---|
| 50 |
|
|---|
| 51 | ## 3. App user + project clone
|
|---|
| 52 |
|
|---|
| 53 | ```bash
|
|---|
| 54 | # As root or via sudo, create a deploy user if you don't already have one
|
|---|
| 55 | # (skip if you're already on a non-root user)
|
|---|
| 56 |
|
|---|
| 57 | # As your user:
|
|---|
| 58 | mkdir -p ~/prutcms
|
|---|
| 59 | cd ~/prutcms
|
|---|
| 60 | # Clone or rsync your code here. e.g. via git:
|
|---|
| 61 | # git clone <your-repo> .
|
|---|
| 62 |
|
|---|
| 63 | npm ci --omit=dev
|
|---|
| 64 | ```
|
|---|
| 65 |
|
|---|
| 66 | `ensureLocalHtmx()` in `server.js` will copy the bundled HTMX from
|
|---|
| 67 | `node_modules/htmx.org/dist/htmx.min.js` to `src/assets/js/htmx.min.js` on
|
|---|
| 68 | first boot. You don't have to do that step manually.
|
|---|
| 69 |
|
|---|
| 70 | ---
|
|---|
| 71 |
|
|---|
| 72 | ## 4. Environment variables
|
|---|
| 73 |
|
|---|
| 74 | Create `.env` in the project root:
|
|---|
| 75 |
|
|---|
| 76 | ```ini
|
|---|
| 77 | NODE_ENV=production
|
|---|
| 78 | PORT=3000
|
|---|
| 79 |
|
|---|
| 80 | # 32+ random hex chars — required, the app refuses to boot without it.
|
|---|
| 81 | # Generate: openssl rand -hex 32
|
|---|
| 82 | SESSION_SECRET=<paste-strong-random-string>
|
|---|
| 83 |
|
|---|
| 84 | # Optional: pin the audio HMAC secret instead of letting the app generate one
|
|---|
| 85 | # in storage/.audio-secret. Generate the same way as SESSION_SECRET.
|
|---|
| 86 | # AUDIO_SECRET=<paste-different-random-string>
|
|---|
| 87 |
|
|---|
| 88 | # Optional: override storage paths
|
|---|
| 89 | # DATABASE_PATH=/home/robin/prutcms/storage/database.sqlite
|
|---|
| 90 | # AUDIO_PATH=/home/robin/prutcms/storage/audio
|
|---|
| 91 | # AVATAR_PATH=/home/robin/prutcms/storage/media/avatars
|
|---|
| 92 | # COVER_PATH=/home/robin/prutcms/storage/media/audio-covers
|
|---|
| 93 | # MEDIA_PATH=/home/robin/prutcms/storage/media
|
|---|
| 94 | ```
|
|---|
| 95 |
|
|---|
| 96 | `chmod 600 .env` — keep it readable only by your user.
|
|---|
| 97 |
|
|---|
| 98 | ---
|
|---|
| 99 |
|
|---|
| 100 | ## 5. First boot
|
|---|
| 101 |
|
|---|
| 102 | ```bash
|
|---|
| 103 | npm run migrate # creates storage/database.sqlite + tables
|
|---|
| 104 | ```
|
|---|
| 105 |
|
|---|
| 106 | Then start with PM2:
|
|---|
| 107 |
|
|---|
| 108 | ```bash
|
|---|
| 109 | pm2 start ecosystem.config.cjs --env production
|
|---|
| 110 | pm2 save
|
|---|
| 111 | pm2 startup # follow the printed command to make PM2 survive reboots
|
|---|
| 112 | ```
|
|---|
| 113 |
|
|---|
| 114 | Check it's up:
|
|---|
| 115 |
|
|---|
| 116 | ```bash
|
|---|
| 117 | curl -I http://127.0.0.1:3000/ # expect 200 / 302
|
|---|
| 118 | pm2 logs prutcms # live logs; Ctrl-C to detach
|
|---|
| 119 | ```
|
|---|
| 120 |
|
|---|
| 121 | Register your first user (becomes god) by visiting
|
|---|
| 122 | `http://YOUR-SERVER-IP:3000/auth/register` BEFORE you point nginx at it
|
|---|
| 123 | (or just wait until SSL is up — registration works the same).
|
|---|
| 124 |
|
|---|
| 125 | ---
|
|---|
| 126 |
|
|---|
| 127 | ## 6. nginx + SSL
|
|---|
| 128 |
|
|---|
| 129 | ```bash
|
|---|
| 130 | sudo cp deploy/nginx.conf.example /etc/nginx/sites-available/prutcms
|
|---|
| 131 | sudo $EDITOR /etc/nginx/sites-available/prutcms # replace <YOUR-DOMAIN>
|
|---|
| 132 | sudo ln -s /etc/nginx/sites-available/prutcms /etc/nginx/sites-enabled/
|
|---|
| 133 | sudo rm -f /etc/nginx/sites-enabled/default
|
|---|
| 134 | sudo nginx -t
|
|---|
| 135 | sudo systemctl reload nginx
|
|---|
| 136 | ```
|
|---|
| 137 |
|
|---|
| 138 | Install certbot and provision certs:
|
|---|
| 139 |
|
|---|
| 140 | ```bash
|
|---|
| 141 | sudo apt install -y certbot python3-certbot-nginx
|
|---|
| 142 | sudo mkdir -p /var/www/letsencrypt
|
|---|
| 143 | sudo certbot --nginx -d YOUR-DOMAIN -d www.YOUR-DOMAIN
|
|---|
| 144 | ```
|
|---|
| 145 |
|
|---|
| 146 | Certbot edits the nginx config in place to wire in the certificate paths.
|
|---|
| 147 | Renewals run automatically via the certbot systemd timer; verify with:
|
|---|
| 148 |
|
|---|
| 149 | ```bash
|
|---|
| 150 | sudo systemctl status certbot.timer
|
|---|
| 151 | ```
|
|---|
| 152 |
|
|---|
| 153 | Now visit `https://YOUR-DOMAIN/`. You should see your site over HTTPS, with
|
|---|
| 154 | HSTS active and Prutter WebSocket working (browser dev-tools → Network →
|
|---|
| 155 | filter "WS" → see the `wss://YOUR-DOMAIN/ws/prutter` connection).
|
|---|
| 156 |
|
|---|
| 157 | ---
|
|---|
| 158 |
|
|---|
| 159 | ## 7. Backups
|
|---|
| 160 |
|
|---|
| 161 | ```bash
|
|---|
| 162 | chmod +x deploy/backup.sh
|
|---|
| 163 | mkdir -p ~/backups/prutcms ~/prutcms/logs
|
|---|
| 164 |
|
|---|
| 165 | # Test it once
|
|---|
| 166 | ./deploy/backup.sh
|
|---|
| 167 |
|
|---|
| 168 | # Schedule nightly at 03:00
|
|---|
| 169 | ( crontab -l 2>/dev/null ; \
|
|---|
| 170 | echo "0 3 * * * /home/$USER/prutcms/deploy/backup.sh >> /home/$USER/prutcms/logs/backup.log 2>&1" \
|
|---|
| 171 | ) | crontab -
|
|---|
| 172 |
|
|---|
| 173 | crontab -l # verify
|
|---|
| 174 | ```
|
|---|
| 175 |
|
|---|
| 176 | Restore is a tar -xzf into a clean directory + `npm ci` + start.
|
|---|
| 177 |
|
|---|
| 178 | ---
|
|---|
| 179 |
|
|---|
| 180 | ## 8. Updating
|
|---|
| 181 |
|
|---|
| 182 | ```bash
|
|---|
| 183 | cd ~/prutcms
|
|---|
| 184 | git pull
|
|---|
| 185 | npm ci --omit=dev
|
|---|
| 186 | pm2 reload prutcms # zero-downtime within fork mode
|
|---|
| 187 | ```
|
|---|
| 188 |
|
|---|
| 189 | The DB schema migrates automatically on boot (`ensureColumn` adds new columns
|
|---|
| 190 | idempotently). For destructive changes you'd need to write an explicit
|
|---|
| 191 | migration — not yet needed.
|
|---|
| 192 |
|
|---|
| 193 | ---
|
|---|
| 194 |
|
|---|
| 195 | ## 9. Multi-tenant setup
|
|---|
| 196 |
|
|---|
| 197 | After your first user/site is created (auto on first registration), use
|
|---|
| 198 | `/admin/sites` to create more sites. Each site gets its own URL prefix
|
|---|
| 199 | (`/sites/<slug>/`) and its own PWA scope, so installing the PWA from one
|
|---|
| 200 | site won't navigate into another.
|
|---|
| 201 |
|
|---|
| 202 | ---
|
|---|
| 203 |
|
|---|
| 204 | ## 10. Troubleshooting
|
|---|
| 205 |
|
|---|
| 206 | | Symptom | Check |
|
|---|
| 207 | |---|---|
|
|---|
| 208 | | `❌ FATAL: SESSION_SECRET is required` | `.env` missing or unreadable. `pm2 stop prutcms && pm2 start ecosystem.config.cjs --env production`. |
|
|---|
| 209 | | `❌ FATAL: SESSION_SECRET too weak for production` | Make it 32+ chars: `openssl rand -hex 32`. |
|
|---|
| 210 | | WS disconnects every minute | Check nginx `proxy_read_timeout` is ≥ 90s in `/ws/` block. |
|
|---|
| 211 | | Audio plays but seek stutters | nginx must have `proxy_buffering off` on `/audio/stream/`. |
|
|---|
| 212 | | 502 from nginx | `pm2 list` — is the app up? `pm2 logs prutcms --lines 100`. |
|
|---|
| 213 | | HTMX 404s on `/assets/js/htmx.min.js` | `ls node_modules/htmx.org/dist/htmx.min.js` — if missing, `npm install htmx.org`. The boot-copy step needs the package. |
|
|---|
| 214 |
|
|---|
| 215 | ---
|
|---|
| 216 |
|
|---|
| 217 | ## 11. What's NOT included
|
|---|
| 218 |
|
|---|
| 219 | - Email sending (password reset prints the URL to the console / page in
|
|---|
| 220 | non-production). Hook up an SMTP or transactional service when needed.
|
|---|
| 221 | - Cluster mode / horizontal scaling. Single fork only — see comments in
|
|---|
| 222 | `ecosystem.config.cjs` for what would have to change first.
|
|---|
| 223 | - Off-site backup replication. The local rotation keeps 14 days; copy the
|
|---|
| 224 | tar.gz files off-server with rsync/restic/whatever you prefer.
|
|---|