aboutsummaryrefslogtreecommitdiffstats
path: root/wh.sh
diff options
context:
space:
mode:
authorFilip Wandzio <contact@philw.dev>2025-10-01 22:21:25 +0200
committerFilip Wandzio <contact@philw.dev>2025-10-01 22:21:25 +0200
commit37e65300245be45d4000797b3ada53c68022fc26 (patch)
treeae304da2869d2069b3359def36e36d29877fc7c8 /wh.sh
parent34fbb07af8de311a4d316325bcf574341f6be5dd (diff)
downloadwhiterabbit-37e65300245be45d4000797b3ada53c68022fc26.tar.gz
whiterabbit-37e65300245be45d4000797b3ada53c68022fc26.zip
Implement use flags for subcommants
Optimize synapse pipeline
Diffstat (limited to 'wh.sh')
-rwxr-xr-xwh.sh500
1 files changed, 500 insertions, 0 deletions
diff --git a/wh.sh b/wh.sh
new file mode 100755
index 0000000..db1c12d
--- /dev/null
+++ b/wh.sh
@@ -0,0 +1,500 @@
1#!/bin/sh
2set -eu
3
4BASE_DIR="/opt/matrix"
5
6DOCKER_COMPOSE=$(command -v docker-compose || command -v docker compose) || {
7 echo "[ERROR] docker-compose or docker compose not found" >&2
8 exit 1
9}
10
11usage() {
12 cat <<'EOF'
13whiterabbit: auto-configure Matrix homeservers (Dendrite or Synapse)
14with a global Coturn instance and Nginx + Let's Encrypt proxy.
15
16Main actions:
17 turn Install or configure the global Coturn server
18 dendrite Add a new Matrix server (Dendrite)
19 synapse Add a new Matrix server (Synapse)
20 list Show all configured Matrix servers
21 remove Remove a Matrix server by domain
22EOF
23 exit 1
24}
25
26require_nonempty() {
27 VAR_NAME="$1"
28 VAR_VALUE="$2"
29 [ -n "$VAR_VALUE" ] || {
30 echo "[ERROR] $VAR_NAME cannot be empty" >&2
31 exit 1
32 }
33}
34
35list_servers() {
36 echo "=== Installed servers ==="
37 for dir in "$BASE_DIR"/*; do
38 [ -d "$dir" ] || continue
39 name=$(basename "$dir")
40
41 status=$(docker ps --filter "name=^${name}$" --format "{{.Status}}")
42 ports=$(docker ps --filter "name=^${name}$" --format "{{.Ports}}" | sed 's/, /\n /g')
43
44 [ -n "$status" ] && {
45 echo "- $name [RUNNING] on:"
46 echo " $ports"
47 } || echo "- $name [STOPPED]"
48 done
49}
50
51install_turn() {
52 echo "=== Installing global Coturn ==="
53 printf "TURN domain (eg. turn.example.com): "
54 read -r TURN_DOMAIN
55 require_nonempty TURN_DOMAIN "$TURN_DOMAIN"
56
57 printf "TURN IP (eg. 127.0.0.1): "
58 read -r TURN_SERVER_IP
59 require_nonempty TURN_SERVER_IP "$TURN_SERVER_IP"
60
61 printf "Listening device (eg. eth0): "
62 read -r TURN_LISTENING_DEVICE
63 require_nonempty TURN_LISTENING_DEVICE "$TURN_LISTENING_DEVICE"
64
65 printf "TURN shared secret: "
66 read -r TURN_SECRET
67 require_nonempty TURN_SECRET "$TURN_SECRET"
68
69 mkdir -p /etc/turn
70 cat <<EOF > /etc/turn/turnserver.conf
71listening-device=$TURN_LISTENING_DEVICE
72listening-port=3478
73tls-listening-port=5349
74listening-ip=$TURN_SERVER_IP
75min-port=49152
76max-port=65535
77use-auth-secret
78static-auth-secret=$TURN_SECRET
79realm=$TURN_DOMAIN
80syslog
81no-rfc5780
82no-stun-backward-compatibility
83response-origin-only-with-rfc5780
84EOF
85
86 echo "$TURN_DOMAIN" > /etc/turn/domain
87 echo "$TURN_SECRET" > /etc/turn/secret
88
89 case $(command -v systemctl >/dev/null && echo y || echo n) in
90 y) systemctl enable --now coturn || true ;;
91 n) service coturn start || true ;;
92 esac
93
94 echo "[OK] Global Coturn configured ($TURN_DOMAIN)"
95}
96
97common_prompts() {
98 printf "Matrix domain (eg. matrix.example.com): "
99 read -r MATRIX_DOMAIN
100 require_nonempty MATRIX_DOMAIN "$MATRIX_DOMAIN"
101
102 printf "Let's Encrypt certificate email: "
103 read -r EMAIL
104 require_nonempty EMAIL "$EMAIL"
105 DB_PASS=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
106 REG_SECRET=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 64)
107 printf "MATRIX server IP (eg. 127.0.0.1): "
108 read -r MATRIX_SERVER_IP
109 require_nonempty MATRIX_SERVER_IP "$MATRIX_SERVER_IP"
110 TURN_DOMAIN=$(cat /etc/turn/domain)
111 TURN_SECRET=$(cat /etc/turn/secret)
112 HASH=$(printf "%s" "$MATRIX_DOMAIN" | cksum | cut -d ' ' -f1)
113 PORT_BASE=$(( (HASH % 1000) + 8000 ))
114 PORT_HTTP=$PORT_BASE
115 PORT_HTTPS=$((PORT_BASE+1))
116}
117
118configure_nginx() {
119 DOMAIN="$1"
120 SERVICE_PORT="$2"
121
122 mkdir -p /etc/nginx/sites-available
123 mkdir -p /etc/nginx/sites-enabled
124
125 cat <<EOF > "/etc/nginx/sites-available/$DOMAIN"
126server {
127 listen 80;
128 server_name $DOMAIN;
129
130 location /.well-known/matrix/server {
131 default_type application/json;
132 return 200 '{ "m.server": "$DOMAIN:443" }';
133 }
134
135 location /.well-known/matrix/client {
136 default_type application/json;
137 return 200 '{
138 "m.homeserver": { "base_url": "https://$DOMAIN" },
139 "m.identity_server": { "base_url": "https://vector.im" }
140 }';
141 }
142
143 location / {
144 proxy_pass http://127.0.0.1:$SERVICE_PORT;
145 proxy_set_header Host \$host;
146 proxy_set_header X-Forwarded-For \$remote_addr;
147 }
148}
149EOF
150
151 ln -sf "/etc/nginx/sites-available/$DOMAIN" "/etc/nginx/sites-enabled/$DOMAIN"
152 nginx -t || { echo "[ERROR] Invalid nginx config"; exit 1; }
153
154
155 case $(command -v systemctl >/dev/null && echo y || echo n) in
156 y) systemctl reload nginx ;;
157 n) nginx -s reload ;;
158 esac
159
160 case $(command -v certbot >/dev/null && echo y || echo n) in
161 y) certbot --nginx --non-interactive --agree-tos -m "$EMAIL" -d "$DOMAIN" || {
162 echo "[WARN] Certbot failed for $DOMAIN"
163 } ;;
164 n) echo "[WARN] Certbot not installed, skipping TLS setup" ;;
165 esac
166}
167
168
169install_dendrite() {
170 echo "=== Installing Matrix Dendrite ==="
171 common_prompts
172
173 INSTALL_DIR="$BASE_DIR/$MATRIX_DOMAIN"
174 mkdir -p "$INSTALL_DIR/config"
175 cd "$INSTALL_DIR" || exit 1
176
177 cat <<EOF > .env
178POSTGRES_HOSTNAME=postgres
179POSTGRES_VERSION=15-alpine
180POSTGRES_USER=dendrite
181POSTGRES_PASSWORD=$DB_PASS
182POSTGRES_DB=dendrite
183
184MONOLITH_HOSTNAME=monolith
185MONOLITH_IMAGE=matrixdotorg/dendrite-monolith:latest
186MONOLITH_PORT_HTTP=$PORT_HTTP
187MONOLITH_PORT_HTTPS=$PORT_HTTPS
188EOF
189
190cat <<EOF > compose.yml
191services:
192 postgres:
193 hostname: \${POSTGRES_HOSTNAME:-postgres}
194 container_name: ${MATRIX_DOMAIN}-db
195 image: postgres:\${POSTGRES_VERSION:-15-alpine}
196 restart: always
197 volumes:
198 - ./dendrite_postgres_data:/var/lib/postgresql/data
199 environment:
200 POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-$DB_PASS}
201 POSTGRES_USER: \${POSTGRES_USER:-dendrite}
202 POSTGRES_DB: \${POSTGRES_DB:-dendrite}
203 healthcheck:
204 test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-dendrite}"]
205 interval: 5s
206 timeout: 5s
207 retries: 5
208 networks:
209 - internal
210
211 monolith:
212 hostname: \${MONOLITH_HOSTNAME:-monolith}
213 container_name: ${MATRIX_DOMAIN}
214 image: \${MONOLITH_IMAGE:-matrixdotorg/dendrite-monolith:latest}
215 ports:
216 - \${MONOLITH_PORT_HTTP:-8008}:8008
217 - \${MONOLITH_PORT_HTTPS:-8448}:8448
218 volumes:
219 - ./config:/etc/dendrite
220 - ./dendrite_media:/var/dendrite/media
221 - ./dendrite_jetstream:/var/dendrite/jetstream
222 - ./dendrite_search_index:/var/dendrite/searchindex
223 - ./:/mnt
224 depends_on:
225 postgres:
226 condition: service_healthy
227 networks:
228 - internal
229 restart: unless-stopped
230
231networks:
232 internal:
233 attachable: true
234volumes:
235 dendrite_postgres_data:
236 dendrite_media:
237 dendrite_jetstream:
238 dendrite_search_index:
239EOF
240
241cat <<EOF > config/dendrite.yaml
242version: 2
243global:
244 server_name: $MATRIX_DOMAIN
245 private_key: /mnt/matrix_key.pem
246 tls_cert: /mnt/server.crt
247 tls_key: /mnt/server.key
248 old_private_keys: []
249 key_validity_period: 168h0m0s
250 database:
251 connection_string: postgresql://dendrite:$DB_PASS@postgres/dendrite?sslmode=disable
252 max_open_conns: 90
253 max_idle_conns: 5
254 conn_max_lifetime: -1
255 well_known_server_name: "$MATRIX_DOMAIN:443"
256 well_known_client_name: "https://$MATRIX_DOMAIN"
257 cache:
258 max_size_estimated: 1gb
259 max_age: 1h
260 trusted_third_party_id_servers:
261 - matrix.org
262 - vector.im
263 disable_federation: false
264 report_stats:
265 enabled: false
266client_api:
267 registration_disabled: true
268 registration_shared_secret: "$REG_SECRET"
269 turn:
270 turn_user_lifetime: "5m"
271 turn_uris:
272 - turn:$TURN_DOMAIN?transport=udp
273 - turn:$TURN_DOMAIN?transport=tcp
274 turn_shared_secret: "$TURN_SECRET"
275federation_api:
276 send_max_retries: 16
277 disable_tls_validation: false
278media_api:
279 base_path: ./media_store
280 max_file_size_bytes: 10485760
281sync_api:
282 search:
283 enabled: false
284 index_path: "./searchindex"
285user_api:
286 auto_join_rooms:
287 - "#main:$MATRIX_DOMAIN"
288logging:
289 - type: std
290 level: info
291 - type: file
292 level: info
293 params:
294 path: ./logs
295jetstream:
296 addresses: []
297 disable_tls_validation: false
298 storage_path: ./
299 topic_prefix: Dendrite
300metrics:
301 enabled: false
302 basic_auth:
303 username: metrics
304 password: metrics
305dns_cache:
306 enabled: false
307 cache_size: 256
308 cache_lifetime: "5m"
309EOF
310
311 echo "[INFO] Generating Dendrite keys..."
312
313 docker run --rm --entrypoint="" -v $(pwd):/mnt matrixdotorg/dendrite-monolith:latest /usr/bin/generate-keys -private-key /mnt/matrix_key.pem -tls-cert /mnt/server.crt -tls-key /mnt/server.key
314 echo "[OK] Keys generated in $PWD"
315
316 $DOCKER_COMPOSE up -d
317 configure_nginx "$MATRIX_DOMAIN" "$PORT_HTTP"
318 echo "[OK] Dendrite server ($MATRIX_DOMAIN) is running."
319}
320
321install_synapse() {
322 echo "=== Installing Matrix Synapse ==="
323 common_prompts
324
325 INSTALL_DIR="$BASE_DIR/$MATRIX_DOMAIN"
326 mkdir -p "$INSTALL_DIR"
327 cd "$INSTALL_DIR" || exit 1
328 mkdir -p data db
329 chown -R 991:991 data/
330 chown -R 999:999 db/
331 DB_USER="synapse"
332 DB_NAME="synapse"
333 DB_PASS=$(openssl rand -hex 16)
334 REG_SECRET=$(openssl rand -hex 32)
335
336 : "${PORT_HTTP:=8008}"
337 : "${PORT_HTTPS:=8448}"
338
339 cat <<EOF > .env
340# Matrix Synapse environment
341MATRIX_DOMAIN=$MATRIX_DOMAIN
342PORT_HTTP=$PORT_HTTP
343PORT_HTTPS=$PORT_HTTPS
344
345DB_USER=$DB_USER
346DB_NAME=$DB_NAME
347DB_PASS=$DB_PASS
348
349REG_SECRET=$REG_SECRET
350TURN_DOMAIN=$TURN_DOMAIN
351TURN_SECRET=$TURN_SECRET
352EOF
353
354
355cat <<EOF > compose.yml
356services:
357 db:
358 image: postgres:15-alpine
359 restart: always
360 environment:
361 POSTGRES_USER: ${DB_USER}
362 POSTGRES_PASSWORD: ${DB_PASS}
363 POSTGRES_DB: ${DB_NAME}
364 LANG: C
365 LC_ALL: C
366 command: postgres -c lc_collate=C -c lc_ctype=C
367 volumes:
368 - ./db:/var/lib/postgresql/data
369 networks:
370 - synapse-net
371
372 synapse:
373 image: matrixdotorg/synapse:latest
374 restart: always
375 depends_on:
376 - db
377 environment:
378 SYNAPSE_SERVER_NAME: ${MATRIX_DOMAIN}
379 SYNAPSE_REPORT_STATS: "yes"
380 ports:
381 - "${PORT_HTTP}:8008"
382 - "${PORT_HTTPS}:8448"
383 volumes:
384 - ./data:/data
385 networks:
386 - synapse-net
387
388networks:
389 synapse-net:
390 driver: bridge
391EOF
392
393$DOCKER_COMPOSE --env-file .env up -d
394docker run -it --rm -v ./data:/data -e SYNAPSE_SERVER_NAME=$MATRIX_DOMAIN -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:latest generate
395
396echo "[INFO] Generating homeserver.yaml for Postgres + TURN..."
397cat <<EOF > data/homeserver.yaml
398server_name: $MATRIX_DOMAIN
399pid_file: /data/homeserver.pid
400
401listeners:
402 - port: $PORT_HTTP
403 tls: false
404 type: http
405 x_forwarded: true
406 resources:
407 - names: [client, federation]
408 compress: false
409
410database:
411 name: psycopg2
412 args:
413 user: $DB_USER
414 password: $DB_PASS
415 host: db
416 database: $DB_NAME
417 cp_min: 5
418 cp_max: 10
419
420enable_registration: false
421registration_shared_secret: "$REG_SECRET"
422
423turn_uris:
424 - turn:$TURN_DOMAIN?transport=udp
425 - turn:$TURN_DOMAIN?transport=tcp
426turn_shared_secret: "$TURN_SECRET"
427turn_user_lifetime: "5m"
428
429log_config: "/data/$MATRIX_DOMAIN.log.config"
430media_store_path: /data/media_store
431report_stats: false
432
433macaroon_secret_key: $REG_SECRET
434form_secret: $REG_SECRET
435signing_key_path: "/data/$MATRIX_DOMAIN.signing.key"
436
437trusted_key_servers:
438 - server_name: "matrix.org"
439suppress_key_server_warning: true
440EOF
441
442 echo "[INFO] Restarting Synapse with full config..."
443 $DOCKER_COMPOSE --env-file .env restart synapse
444 $DOCKER_COMPOSE --env-file .env restart synapse
445
446 configure_nginx "$MATRIX_DOMAIN" "$PORT_HTTP"
447 echo "[OK] Synapse server ($MATRIX_DOMAIN) is running with Postgres."
448}
449
450
451remove_server() {
452 [ "$#" -eq 0 ] && echo "Usage: remove_server <domain1> [domain2 ...]" && return 1
453
454 for DOMAIN in "$@"; do
455 SERVER_DIR="$BASE_DIR/$DOMAIN"
456
457 case "$DOMAIN" in
458 ""|"/"|".")
459 echo "[ERROR] Refusing to delete dangerous path ($DOMAIN)"
460 continue
461 ;;
462 esac
463
464 [ -d "$SERVER_DIR" ] && \
465 echo "[INFO] Stopping and removing server: $DOMAIN" && \
466 (cd "$SERVER_DIR" && $DOCKER_COMPOSE down -v) || \
467 echo "[WARN] Failed to stop container for $DOMAIN"
468
469 rm -rf "$SERVER_DIR"
470 rm -f "/etc/nginx/sites-available/$DOMAIN" "/etc/nginx/sites-enabled/$DOMAIN"
471 done
472
473 nginx -t && (systemctl reload nginx 2>/dev/null || nginx -s reload)
474 echo "[OK] Done removing servers: $*"
475}
476
477CMD="${1:-}"
478
479case "$CMD" in
480 -t) install_turn ;;
481 -d) install_dendrite ;;
482 -s) install_synapse ;;
483 -l) list_servers ;;
484 -r)
485 [ -z "$2" ] && { echo "Usage: $0 -r <domain1> [domain2 ...]"; exit 1; }
486 shift
487 remove_server "$@"
488 ;;
489 turn) install_turn ;;
490 dendrite) install_dendrite ;;
491 synapse) install_synapse ;;
492 list) list_servers ;;
493 remove)
494 [ -z "$2" ] && { echo "Usage: $0 remove <domain1> [domain2 ...]"; exit 1; }
495 shift
496 remove_server "$@"
497 ;;
498 *) usage ;;
499esac
500