| 1 | # Klonkt — 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 ~/klonkt
|
|---|
| 59 | cd ~/klonkt
|
|---|
| 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/klonkt/storage/database.sqlite
|
|---|
| 90 | # AUDIO_PATH=/home/robin/klonkt/storage/audio
|
|---|
| 91 | # AVATAR_PATH=/home/robin/klonkt/storage/media/avatars
|
|---|
| 92 | # COVER_PATH=/home/robin/klonkt/storage/media/audio-covers
|
|---|
| 93 | # MEDIA_PATH=/home/robin/klonkt/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 klonkt # 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/klonkt
|
|---|
| 131 | sudo $EDITOR /etc/nginx/sites-available/klonkt # replace <YOUR-DOMAIN>
|
|---|
| 132 | sudo ln -s /etc/nginx/sites-available/klonkt /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.
|
|---|
| 155 |
|
|---|
| 156 | ---
|
|---|
| 157 |
|
|---|
| 158 | ## 7. Backups
|
|---|
| 159 |
|
|---|
| 160 | ```bash
|
|---|
| 161 | chmod +x deploy/backup.sh
|
|---|
| 162 | mkdir -p ~/backups/klonkt ~/klonkt/logs
|
|---|
| 163 |
|
|---|
| 164 | # Test it once
|
|---|
| 165 | ./deploy/backup.sh
|
|---|
| 166 |
|
|---|
| 167 | # Schedule nightly at 03:00
|
|---|
| 168 | ( crontab -l 2>/dev/null ; \
|
|---|
| 169 | echo "0 3 * * * /home/$USER/klonkt/deploy/backup.sh >> /home/$USER/klonkt/logs/backup.log 2>&1" \
|
|---|
| 170 | ) | crontab -
|
|---|
| 171 |
|
|---|
| 172 | crontab -l # verify
|
|---|
| 173 | ```
|
|---|
| 174 |
|
|---|
| 175 | Restore is a tar -xzf into a clean directory + `npm ci` + start.
|
|---|
| 176 |
|
|---|
| 177 | ---
|
|---|
| 178 |
|
|---|
| 179 | ## 8. Updating
|
|---|
| 180 |
|
|---|
| 181 | ```bash
|
|---|
| 182 | cd ~/klonkt
|
|---|
| 183 | git pull
|
|---|
| 184 | npm ci --omit=dev
|
|---|
| 185 | pm2 reload klonkt # zero-downtime within fork mode
|
|---|
| 186 | ```
|
|---|
| 187 |
|
|---|
| 188 | The DB schema migrates automatically on boot (`ensureColumn` adds new columns
|
|---|
| 189 | idempotently). For destructive changes you'd need to write an explicit
|
|---|
| 190 | migration — not yet needed.
|
|---|
| 191 |
|
|---|
| 192 | ---
|
|---|
| 193 |
|
|---|
| 194 | ## 9. Multi-tenant setup
|
|---|
| 195 |
|
|---|
| 196 | After your first user/site is created (auto on first registration), use
|
|---|
| 197 | `/admin/sites` to create more sites. Each site gets its own URL prefix
|
|---|
| 198 | (`/sites/<slug>/`) and its own PWA scope, so installing the PWA from one
|
|---|
| 199 | site won't navigate into another.
|
|---|
| 200 |
|
|---|
| 201 | These sites live inside one Klonkt, sharing one database and one process. For
|
|---|
| 202 | separate domains with separate databases, see the next section instead.
|
|---|
| 203 |
|
|---|
| 204 | ---
|
|---|
| 205 |
|
|---|
| 206 | ## 9b. Several independent Klonkts on one server
|
|---|
| 207 |
|
|---|
| 208 | A different question from the one above: not several sites inside one Klonkt,
|
|---|
| 209 | but several Klonkts side by side, each with its own domain, database and
|
|---|
| 210 | uploads, all sharing a single copy of the code.
|
|---|
| 211 |
|
|---|
| 212 | That layout keeps user data out of the checkout:
|
|---|
| 213 |
|
|---|
| 214 | ```
|
|---|
| 215 | /opt/klonkt/ the code, shared by every instance
|
|---|
| 216 | /var/lib/klonkt/<slug>/ one instance: .env, database, uploads
|
|---|
| 217 | ```
|
|---|
| 218 |
|
|---|
| 219 | Adding a site is then a directory plus an `.env`, and `klonkt-update` moves all
|
|---|
| 220 | of them to the new code in one step. Existing single site installs can be
|
|---|
| 221 | converted with `scripts/klonkt-migrate-data.sh`.
|
|---|
| 222 |
|
|---|
| 223 | See **[MULTI-INSTANCE.md](MULTI-INSTANCE.md)** for the layout, the migration,
|
|---|
| 224 | the file ownership and the backup routine.
|
|---|
| 225 |
|
|---|
| 226 | ---
|
|---|
| 227 |
|
|---|
| 228 | ## 10. Troubleshooting
|
|---|
| 229 |
|
|---|
| 230 | | Symptom | Check |
|
|---|
| 231 | |---|---|
|
|---|
| 232 | | `❌ FATAL: SESSION_SECRET is required` | `.env` missing or unreadable. `pm2 stop klonkt && pm2 start ecosystem.config.cjs --env production`. |
|
|---|
| 233 | | `❌ FATAL: SESSION_SECRET too weak for production` | Make it 32+ chars: `openssl rand -hex 32`. |
|
|---|
| 234 | | WS disconnects every minute | Check nginx `proxy_read_timeout` is ≥ 90s in `/ws/` block. |
|
|---|
| 235 | | Audio plays but seek stutters | nginx must have `proxy_buffering off` on `/audio/stream/`. |
|
|---|
| 236 | | 502 from nginx | `pm2 list` — is the app up? `pm2 logs klonkt --lines 100`. |
|
|---|
| 237 | | 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. |
|
|---|
| 238 |
|
|---|
| 239 | ---
|
|---|
| 240 |
|
|---|
| 241 | ## 11. What's NOT included
|
|---|
| 242 |
|
|---|
| 243 | - Email sending (password reset prints the URL to the console / page in
|
|---|
| 244 | non-production). Hook up an SMTP or transactional service when needed.
|
|---|
| 245 | - Cluster mode / horizontal scaling. Single fork only — see comments in
|
|---|
| 246 | `ecosystem.config.cjs` for what would have to change first.
|
|---|
| 247 | - Off-site backup replication. The local rotation keeps 14 days; copy the
|
|---|
| 248 | tar.gz files off-server with rsync/restic/whatever you prefer.
|
|---|