#!/usr/bin/env bash # shellcheck shell=bash # entrypoint.sh — container startup script # Called by: tini → entrypoint.sh → prosody set -euo pipefail # - - - - - - - - - - - - - - - - - - - - - - - - - # Defaults # - - - - - - - - - - - - - - - - - - - - - - - - - : "${XMPP_DOMAIN:=meet.jitsi}" : "${XMPP_AUTH_DOMAIN:=auth.meet.jitsi}" : "${XMPP_GUEST_DOMAIN:=guest.meet.jitsi}" : "${XMPP_MUC_DOMAIN:=muc.meet.jitsi}" : "${XMPP_INTERNAL_MUC_DOMAIN:=internal-muc.meet.jitsi}" : "${XMPP_HIDDEN_DOMAIN:=hidden.meet.jitsi}" : "${XMPP_PORT:=5222}" : "${PROSODY_HTTP_PORT:=5280}" : "${ENABLE_AUTH:=0}" : "${ENABLE_GUESTS:=0}" : "${ENABLE_XMPP_WEBSOCKET:=1}" : "${ENABLE_LOBBY:=1}" : "${ENABLE_BREAKOUT_ROOMS:=1}" : "${ENABLE_AV_MODERATION:=1}" : "${ENABLE_END_CONFERENCE:=1}" : "${ENABLE_RECORDING:=0}" : "${ENABLE_TRANSCRIPTIONS:=0}" : "${JVB_AUTH_USER:=jvb}" : "${JIBRI_RECORDER_USER:=recorder}" : "${JIBRI_XMPP_USER:=jibri}" : "${JIGASI_XMPP_USER:=jigasi}" : "${LOG_LEVEL:=info}" : "${PROSODY_MODE:=client}" : "${PROSODY_C2S_REQUIRE_ENCRYPTION:=true}" : "${PROSODY_ADMINS:=}" : "${AUTH_TYPE:=internal}" : "${DISABLE_POLLS:=0}" # Derived XMPP_MUC_DOMAIN_PREFIX="${XMPP_MUC_DOMAIN%%.*}" PROSODY_CFG="/config/prosody.cfg.lua" PROSODY_SITE_CFG="/config/conf.d/jitsi-meet.cfg.lua" # - - - - - - - - - - - - - - - - - - - - - - - - - # Guards # - - - - - - - - - - - - - - - - - - - - - - - - - [[ -n "${JICOFO_AUTH_PASSWORD:-}" ]] || { echo "FATAL: JICOFO_AUTH_PASSWORD must be set" >&2; exit 1; } [[ -n "${JVB_AUTH_PASSWORD:-}" ]] || { echo "FATAL: JVB_AUTH_PASSWORD must be set" >&2; exit 1; } # - - - - - - - - - - - - - - - - - - - - - - - - - # Directory setup # - - - - - - - - - - - - - - - - - - - - - - - - - mkdir -p /config/certs /config/conf.d /config/data \ /prosody-plugins-custom /prosody-plugins-contrib chown -R prosody:prosody /config /prosody-plugins-custom /prosody-plugins-contrib 2>/dev/null || true # - - - - - - - - - - - - - - - - - - - - - - - - - # Generate main prosody.cfg.lua # - - - - - - - - - - - - - - - - - - - - - - - - - __generate_main_cfg() { # Build admins list local admins_list="" if [[ -n "${PROSODY_ADMINS}" ]]; then while IFS=',' read -ra parts; do for admin in "${parts[@]}"; do admin="${admin// /}" [[ -n "$admin" ]] && admins_list+=" \"${admin}\";\n" done done <<< "${PROSODY_ADMINS}" fi cat > "${PROSODY_CFG}" << MAIN_CFG -- Generated by entrypoint.sh — do not edit manually ---------- Server-wide settings ---------- admins = { $(printf '%b' "${admins_list}")} component_admins_as_room_owners = true modules_enabled = { -- Core "roster"; "saslauth"; "tls"; "disco"; "ping"; "version"; "posix"; "limits"; "private"; -- HTTP (BOSH + WebSocket — required for Jitsi) "bosh"; "websocket"; "http"; "http_health"; -- Status "uptime"; }; modules_disabled = { "offline"; "register"; "s2s"; }; -- Never allow unauthenticated registration globally allow_registration = false; -- Rate limits for incoming connections limits = { c2s = { rate = "10kb/s"; }; }; -- Garbage collector gc = { mode = "incremental"; threshold = 400; speed = 250; step_size = 13; }; pidfile = "/config/data/prosody.pid"; c2s_require_encryption = ${PROSODY_C2S_REQUIRE_ENCRYPTION}; c2s_ports = { ${XMPP_PORT} } c2s_interfaces = { "*" } s2s_secure_auth = false authentication = "internal_hashed" -- Logging log = { { levels = { min = "${LOG_LEVEL}" }, timestamps = "%Y-%m-%d %X", to = "console" }; }; -- HTTP configuration -- consider_bosh_secure and consider_websocket_secure allow HTTPS-terminated -- connections from a reverse proxy to be treated as secure. consider_bosh_secure = true; consider_websocket_secure = true; -- Allow WebSocket + BOSH on the same port (5280) that the reverse proxy reaches http_ports = { ${PROSODY_HTTP_PORT} } http_interfaces = { "*" } -- Stream management (required for smacks / WebSocket reconnect) smacks_max_unacked_stanzas = 5; smacks_hibernation_time = 60; smacks_max_old_sessions = 1; -- epoll backend for better performance network_backend = "epoll"; network_settings = { tcp_backlog = 511; }; -- Data storage data_path = "/config/data"; -- Plugin paths plugin_paths = { "/prosody-plugins-custom", "/prosody-plugins/", "/prosody-plugins-contrib" }; Include "conf.d/*.cfg.lua" MAIN_CFG } # - - - - - - - - - - - - - - - - - - - - - - - - - # Generate conf.d/jitsi-meet.cfg.lua # - - - - - - - - - - - - - - - - - - - - - - - - - __generate_site_cfg() { # Determine main VirtualHost authentication local main_auth="jitsi-anonymous" if [[ "${ENABLE_AUTH}" == "1" ]] || [[ "${ENABLE_AUTH}" == "true" ]]; then main_auth="internal_hashed" fi # Determine guest VirtualHost authentication local guest_auth="jitsi-anonymous" # Build global admins block for conf.d cat > "${PROSODY_SITE_CFG}" << SITE_START -- Generated by entrypoint.sh — do not edit manually admins = { "focus@${XMPP_AUTH_DOMAIN}", "${JVB_AUTH_USER}@${XMPP_AUTH_DOMAIN}" } unlimited_jids = { "focus@${XMPP_AUTH_DOMAIN}", "${JVB_AUTH_USER}@${XMPP_AUTH_DOMAIN}" } muc_mapper_domain_base = "${XMPP_DOMAIN}"; muc_mapper_domain_prefix = "${XMPP_MUC_DOMAIN_PREFIX}"; recorder_prefixes = { "${JIBRI_RECORDER_USER}@${XMPP_HIDDEN_DOMAIN}" }; transcriber_prefixes = { "${JIGASI_TRANSCRIBER_USER:-transcriber}@${XMPP_HIDDEN_DOMAIN}" }; http_default_host = "${XMPP_DOMAIN}" -- Main virtual host VirtualHost "${XMPP_DOMAIN}" authentication = "${main_auth}" ssl = { key = "/config/certs/${XMPP_DOMAIN}.key"; certificate = "/config/certs/${XMPP_DOMAIN}.crt"; } modules_enabled = { "bosh"; "websocket"; "smacks"; "conference_duration"; "muc_lobby_rooms"; "muc_breakout_rooms"; "features_identity"; SITE_START # Optional XMPP_MODULES if [[ -n "${XMPP_MODULES:-}" ]]; then while IFS=',' read -ra mods; do for mod in "${mods[@]}"; do mod="${mod// /}" [[ -n "$mod" ]] && printf ' "%s";\n' "${mod}" >> "${PROSODY_SITE_CFG}" done done <<< "${XMPP_MODULES}" fi cat >> "${PROSODY_SITE_CFG}" << SITE_MAIN_HOST } main_muc = "${XMPP_MUC_DOMAIN}" lobby_muc = "lobby.${XMPP_DOMAIN}" breakout_rooms_muc = "breakout.${XMPP_DOMAIN}" c2s_require_encryption = ${PROSODY_C2S_REQUIRE_ENCRYPTION} SITE_MAIN_HOST # Guest domain (only when auth is enabled and guests are enabled) if { [[ "${ENABLE_AUTH}" == "1" ]] || [[ "${ENABLE_AUTH}" == "true" ]]; } && \ { [[ "${ENABLE_GUESTS}" == "1" ]] || [[ "${ENABLE_GUESTS}" == "true" ]]; }; then cat >> "${PROSODY_SITE_CFG}" << SITE_GUEST VirtualHost "${XMPP_GUEST_DOMAIN}" authentication = "${guest_auth}" modules_enabled = { "smacks"; } main_muc = "${XMPP_MUC_DOMAIN}" lobby_muc = "lobby.${XMPP_DOMAIN}" breakout_rooms_muc = "breakout.${XMPP_DOMAIN}" c2s_require_encryption = ${PROSODY_C2S_REQUIRE_ENCRYPTION} SITE_GUEST fi # Auth domain cat >> "${PROSODY_SITE_CFG}" << SITE_AUTH VirtualHost "${XMPP_AUTH_DOMAIN}" ssl = { key = "/config/certs/${XMPP_AUTH_DOMAIN}.key"; certificate = "/config/certs/${XMPP_AUTH_DOMAIN}.crt"; } modules_enabled = { "limits_exception"; "smacks"; } authentication = "internal_hashed" smacks_hibernation_time = 15; SITE_AUTH # Hidden domain for recording/transcription if [[ "${ENABLE_RECORDING}" == "1" ]] || [[ "${ENABLE_RECORDING}" == "true" ]] || \ [[ "${ENABLE_TRANSCRIPTIONS}" == "1" ]] || [[ "${ENABLE_TRANSCRIPTIONS}" == "true" ]]; then cat >> "${PROSODY_SITE_CFG}" << SITE_HIDDEN VirtualHost "${XMPP_HIDDEN_DOMAIN}" modules_enabled = { "smacks"; } authentication = "internal_hashed" SITE_HIDDEN fi # Components cat >> "${PROSODY_SITE_CFG}" << SITE_COMPONENTS -- Internal MUC (JVB/jicofo signaling — never exposed to clients) Component "${XMPP_INTERNAL_MUC_DOMAIN}" "muc" storage = "memory" modules_enabled = { "muc_hide_all"; "muc_filter_access"; } restrict_room_creation = true muc_filter_whitelist = "${XMPP_AUTH_DOMAIN}" muc_room_locking = false muc_room_default_public_jids = true muc_room_cache_size = 1000 muc_tombstones = false muc_room_allow_persistent = false -- Conference MUC (participants join rooms here) Component "${XMPP_MUC_DOMAIN}" "muc" restrict_room_creation = true storage = "memory" modules_enabled = { "muc_hide_all"; "muc_meeting_id"; "muc_domain_mapper"; "muc_password_whitelist"; } muc_room_cache_size = 10000 muc_room_locking = false muc_room_default_public_jids = true muc_tombstones = false muc_room_allow_persistent = false muc_password_whitelist = { "focus@${XMPP_AUTH_DOMAIN}"; } -- Focus component proxy Component "focus.${XMPP_DOMAIN}" "client_proxy" target_address = "focus@${XMPP_AUTH_DOMAIN}" -- Speakerstats component Component "speakerstats.${XMPP_DOMAIN}" "speakerstats_component" muc_component = "${XMPP_MUC_DOMAIN}" -- End conference component Component "endconference.${XMPP_DOMAIN}" "end_conference" muc_component = "${XMPP_MUC_DOMAIN}" -- AV moderation component Component "avmoderation.${XMPP_DOMAIN}" "av_moderation_component" muc_component = "${XMPP_MUC_DOMAIN}" -- Lobby MUC Component "lobby.${XMPP_DOMAIN}" "muc" storage = "memory" restrict_room_creation = true muc_tombstones = false muc_room_allow_persistent = false muc_room_cache_size = 10000 muc_room_locking = false muc_room_default_public_jids = true modules_enabled = { "muc_hide_all"; } -- Breakout rooms MUC Component "breakout.${XMPP_DOMAIN}" "muc" storage = "memory" restrict_room_creation = true muc_room_cache_size = 10000 muc_room_locking = false muc_room_default_public_jids = true muc_tombstones = false muc_room_allow_persistent = false modules_enabled = { "muc_hide_all"; "muc_meeting_id"; } -- Room metadata component Component "metadata.${XMPP_DOMAIN}" "room_metadata_component" muc_component = "${XMPP_MUC_DOMAIN}" breakout_rooms_component = "breakout.${XMPP_DOMAIN}" SITE_COMPONENTS # Polls component (optional disable) if [[ "${DISABLE_POLLS}" != "1" ]] && [[ "${DISABLE_POLLS}" != "true" ]]; then cat >> "${PROSODY_SITE_CFG}" << SITE_POLLS Component "polls.${XMPP_DOMAIN}" "polls_component" SITE_POLLS fi } # - - - - - - - - - - - - - - - - - - - - - - - - - # Certificate generation # - - - - - - - - - - - - - - - - - - - - - - - - - __generate_cert() { local domain="${1}" local cert_dir="/config/certs" local key="${cert_dir}/${domain}.key" local crt="${cert_dir}/${domain}.crt" [[ -f "${key}" && -f "${crt}" ]] && return 0 echo "INFO: Generating self-signed certificate for ${domain}" openssl req -x509 -newkey rsa:4096 \ -keyout "${key}" -out "${crt}" \ -days 3650 -nodes \ -subj "/CN=${domain}" 2>/dev/null chmod 600 "${key}" chown prosody:prosody "${key}" "${crt}" 2>/dev/null || true } __generate_certs() { __generate_cert "${XMPP_DOMAIN}" __generate_cert "${XMPP_AUTH_DOMAIN}" } # - - - - - - - - - - - - - - - - - - - - - - - - - # User registration # - - - - - - - - - - - - - - - - - - - - - - - - - __register_user() { local user="${1}" domain="${2}" pass="${3}" # prosodyctl register is idempotent — re-register just updates the password su-exec prosody prosodyctl --config "${PROSODY_CFG}" \ register "${user}" "${domain}" "${pass}" 2>/dev/null || true } __register_jitsi_users() { # Start prosody temporarily in background so prosodyctl can connect su-exec prosody prosody --config "${PROSODY_CFG}" -F & local pid=$! # Wait until prosody is accepting connections (up to 30s) local i=0 while ! su-exec prosody prosodyctl --config "${PROSODY_CFG}" status >/dev/null 2>&1; do sleep 1 i=$(( i + 1 )) if (( i >= 30 )); then echo "FATAL: prosody did not start within 30s" >&2 kill "${pid}" 2>/dev/null || true exit 1 fi done echo "INFO: Registering jicofo user" __register_user "focus" "${XMPP_AUTH_DOMAIN}" "${JICOFO_AUTH_PASSWORD}" echo "INFO: Registering jvb user" __register_user "${JVB_AUTH_USER}" "${XMPP_AUTH_DOMAIN}" "${JVB_AUTH_PASSWORD}" # Subscribe focus to the focus component so roster is correct if [[ "${PROSODY_MODE}" == "client" ]]; then su-exec prosody prosodyctl --config "${PROSODY_CFG}" \ mod_roster_command subscribe "focus.${XMPP_DOMAIN}" "focus@${XMPP_AUTH_DOMAIN}" 2>/dev/null || true fi # Optional: jibri user if [[ -n "${JIBRI_XMPP_PASSWORD:-}" ]]; then echo "INFO: Registering jibri user" __register_user "${JIBRI_XMPP_USER}" "${XMPP_AUTH_DOMAIN}" "${JIBRI_XMPP_PASSWORD}" fi # Optional: jigasi user if [[ -n "${JIGASI_XMPP_PASSWORD:-}" ]]; then echo "INFO: Registering jigasi user" __register_user "${JIGASI_XMPP_USER}" "${XMPP_AUTH_DOMAIN}" "${JIGASI_XMPP_PASSWORD}" fi # Shut down the temporary instance kill "${pid}" 2>/dev/null || true wait "${pid}" 2>/dev/null || true # Clean up pid file so the main instance can start rm -f /config/data/prosody.pid sleep 1 } # - - - - - - - - - - - - - - - - - - - - - - - - - # Main # - - - - - - - - - - - - - - - - - - - - - - - - - echo "INFO: Generating prosody configuration" __generate_main_cfg __generate_site_cfg __generate_certs __register_jitsi_users echo "INFO: Starting prosody" exec su-exec prosody prosody --config "${PROSODY_CFG}" -F