🦈🏠🐜 Initial Commit 🐜🦈🏠

This commit is contained in:
2026-05-19 12:54:07 -04:00
commit dfd79d5774
14 changed files with 986 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# .dockerignore created on 05/19/26 at 09:30
# version control
.git/
.gitignore
.gitattributes
# local and secret config
.env
app.env
default.env
.claude/
# build artifacts
binaries/
releases/
# runtime volume data (never in image)
volumes/
docker/volumes/
# OS files
.DS_Store
Thumbs.db
# docs and meta (not needed in image)
*.md
LICENSE*
+98
View File
@@ -0,0 +1,98 @@
# Template generated on Sun May 17 10:58:44 PM EDT 2026 from https://github.com/alexkaratarakis/gitattributes"
# Common settings that generally should always be used with your language specific settings
# Auto detect text files and perform LF normalization
* text=auto
# The above will handle all files NOT found below
# Documents
*.bibtex text diff=bibtex
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.md text diff=markdown
*.mdx text diff=markdown
*.tex text diff=tex
*.adoc text
*.textile text
*.mustache text
*.csv text eol=crlf
*.tab text
*.tsv text
*.txt text
*.sql text
*.epub diff=astextplain
# Graphics
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.tif binary
*.tiff binary
*.ico binary
# SVG treated as text by default.
*.svg text
# If you want to treat it as binary,
# use the following line instead.
# *.svg binary
*.eps binary
# Scripts
*.bash text eol=lf
*.fish text eol=lf
*.ksh text eol=lf
*.sh text eol=lf
*.zsh text eol=lf
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Serialisation
*.json text
*.toml text
*.xml text
*.yaml text
*.yml text
# Archives
*.7z binary
*.bz binary
*.bz2 binary
*.bzip2 binary
*.gz binary
*.lz binary
*.lzma binary
*.rar binary
*.tar binary
*.taz binary
*.tbz binary
*.tbz2 binary
*.tgz binary
*.tlz binary
*.txz binary
*.xz binary
*.Z binary
*.zip binary
*.zst binary
# Text files where line endings should be preserved
*.patch -text
# Exclude files from exporting
.gitattributes export-ignore
.gitignore export-ignore
.gitkeep export-ignore
# Template generated on Sun May 17 10:58:44 PM EDT 2026
# Files for git large file system
*.7z filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.xz filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
*.bz2 filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.rpm filter=lfs diff=lfs merge=lfs -text
*.7zip filter=lfs diff=lfs merge=lfs -text
*.bzip2 filter=lfs diff=lfs merge=lfs -text
+8
View File
@@ -0,0 +1,8 @@
# Global owners — review required for everything not covered below
* @casjaysdevdocker
# Workflow changes require maintainer sign-off
.github/ @casjaysdevdocker
# Docker assets
docker/ @casjaysdevdocker
+58
View File
@@ -0,0 +1,58 @@
name: CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
contents: read
packages: write
jobs:
build:
name: Build image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: docker/setup-buildx-action@b5730e1a0a27db01e64c4ab5f4f7c8a63c1de14a # v3.10.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@902fa8ec7d6ecbea8a986b37db18de1c4e72470b # v5.7.0
id: meta
with:
images: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
tags: |
type=edge,branch=main
type=sha,prefix=sha-
annotations: |
maintainer=${{ github.repository_owner }} <${{ github.repository_owner }}@casjay.pro>
org.opencontainers.image.vendor=${{ github.repository_owner }}
org.opencontainers.image.authors=${{ github.repository_owner }}
org.opencontainers.image.title=${{ github.event.repository.name }}
org.opencontainers.image.description=Containerized version of ${{ github.event.repository.name }}
org.opencontainers.image.url=${{ github.event.repository.html_url }}
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.documentation=${{ github.event.repository.html_url }}
org.opencontainers.image.vcs-type=Git
com.github.containers.toolbox=false
- uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with:
context: .
file: docker/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
labels: ""
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
+71
View File
@@ -0,0 +1,71 @@
name: Release
on:
push:
tags: ["v*"]
permissions:
contents: write
packages: write
jobs:
release:
name: Build and publish release image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: docker/setup-buildx-action@b5730e1a0a27db01e64c4ab5f4f7c8a63c1de14a # v3.10.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- uses: docker/metadata-action@902fa8ec7d6ecbea8a986b37db18de1c4e72470b # v5.7.0
id: meta
with:
images: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
type=raw,value=${{ steps.version.outputs.version }}
annotations: |
maintainer=${{ github.repository_owner }} <${{ github.repository_owner }}@casjay.pro>
org.opencontainers.image.vendor=${{ github.repository_owner }}
org.opencontainers.image.authors=${{ github.repository_owner }}
org.opencontainers.image.title=${{ github.event.repository.name }}
org.opencontainers.image.description=Containerized version of ${{ github.event.repository.name }}
org.opencontainers.image.url=${{ github.event.repository.html_url }}
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.documentation=${{ github.event.repository.html_url }}
org.opencontainers.image.vcs-type=Git
com.github.containers.toolbox=false
- uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with:
context: .
file: docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
labels: ""
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.version.outputs.version }}
BUILD_DATE=${{ github.event.head_commit.timestamp }}
VCS_REF=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Create GitHub Release
uses: softprops/action-gh-release@da05d552573ad5aba039edc1654bcb6e02e54e87 # v2.2.2
with:
generate_release_notes: true
tag_name: ${{ github.ref_name }}
+69
View File
@@ -0,0 +1,69 @@
# gitignore created on 05/19/26 at 09:30
ignoredirmessage
# ignore commit message
**/.gitcommit
# ignore build failure markers
**/.build_failed*
# ignore backup files
**/*.bak
# ignore push/git control files
**/.no_push
**/.no_git
# ignore install markers
**/.installed
# ignore work in progress scripts
**/*.rewrite.sh
**/*.refactor.sh
# ignore dotenv files
.env
app.env
default.env
# ignore log and temp files
*.log
*.tmp
*.temp
# OS generated files
### Linux ###
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
### macOS ###
.DS_Store?
.AppleDouble
.LSOverride
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
# runtime volume data — never committed
volumes/
docker/volumes/
# Claude Code runtime files
.claude/settings.local.json
.claude/backups/
.claude/cache/
.claude/file-history/
.claude/history.jsonl
.claude/projects/
.claude/statsFile
.claude/*.lock
+7
View File
@@ -0,0 +1,7 @@
# Claude loader
Project spec: [IDEA.md](IDEA.md)
This is a Docker image project. Implementation conventions come from
`~/.claude/memory/dockerfile_conventions.md`. All Docker assets live
under `docker/`.
+32
View File
@@ -0,0 +1,32 @@
## Project description
A production-ready Docker image for [Prosody XMPP](https://prosody.im/) configured specifically for Jitsi Meet deployments. Replaces the upstream `jitsi/docker-jitsi-meet` prosody image with a standards-compliant CasjaysDev image built on Alpine Linux with full BOSH and WebSocket support (mod_websocket).
## Project variables
```
project_name: prosody
project_org: casjaysdevdocker
internal_name: prosody
internal_org: casjaysdevdocker
image_registry: ghcr.io
upstream_image: prosody (Alpine package)
jitsi_repo: https://github.com/jitsi/docker-jitsi-meet
```
## Business logic
- Must be a drop-in replacement for `jitsi/docker-jitsi-meet` prosody container
- Must support all standard Jitsi Docker env vars: `XMPP_DOMAIN`, `XMPP_AUTH_DOMAIN`, `XMPP_MUC_DOMAIN`, `XMPP_INTERNAL_MUC_DOMAIN`, `XMPP_GUEST_DOMAIN`, `JICOFO_AUTH_PASSWORD`, `JVB_AUTH_PASSWORD`, `ENABLE_AUTH`, `ENABLE_GUESTS`, `ENABLE_XMPP_WEBSOCKET`, `PUBLIC_URL`, `LOG_LEVEL`
- Must have BOSH working on `/http-bind` at port 5280
- Must have WebSocket working on `/xmpp-websocket` at port 5280 (this is the primary fix over upstream)
- Must register jicofo and jvb users automatically at startup
- Must generate self-signed TLS certificates for Jitsi domains at startup if not present
- Must expose ports 5222 (c2s), 5280 (HTTP/BOSH/WS), 5347 (component)
- Must include all Jitsi prosody plugins from `jitsi/docker-jitsi-meet` prosody image
- Must run prosody as the `prosody` system user (non-root), not as root
- Must use `tini` as PID 1
- Must work with zero config — sane defaults for all env vars
- Must NOT require a separate nginx reverse proxy in the container
- Must NOT include s6 or any other process supervisor — single process per container
- `consider_websocket_secure = true` and `consider_bosh_secure = true` must always be set so trusted-proxy HTTPS termination works
+13
View File
@@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2026 casjay <git-admin@casjaysdev.pro>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
1. You just DO WHAT THE FUCK YOU WANT TO.
+8
View File
@@ -0,0 +1,8 @@
## 👋 Welcome to prosody 🚀
prosody README
## Author
🤖 casjay: [Github](https://github.com/casjay) 🤖
+69
View File
@@ -0,0 +1,69 @@
# =============================================================================
# Jitsi Source Stage — extract plugins and pure-Lua libs from the official
# jitsi/prosody image at build time so we never fetch at runtime
# =============================================================================
FROM jitsi/prosody:stable AS jitsi-source
# =============================================================================
# Runtime Stage
# =============================================================================
FROM alpine:latest
ARG VERSION=dev
ARG BUILD_DATE
ARG VCS_REF
ARG LICENSE=MIT
# Install runtime dependencies
# prosody from Alpine 13.x matches jitsi/prosody:stable (also 13.x)
# su-exec drops from root to prosody user before exec
RUN apk add --no-cache \
bash \
curl \
openssl \
prosody \
su-exec \
tini
# Apply prosody 13.x compatibility patch:
# idna_to_ascii was removed in libicu 72+ but the Lua util still references it;
# the jitsi base image applies this same patch (sed -i '/idna_to_ascii/d')
RUN sed -i '/idna_to_ascii/d' /usr/lib/prosody/util/jid.lua 2>/dev/null || true
# Create runtime directories
RUN mkdir -p \
/config/certs \
/config/conf.d \
/config/data \
/prosody-plugins \
/prosody-plugins-contrib \
/prosody-plugins-custom
# Copy Jitsi-specific prosody plugins (pure Lua — architecture-independent)
COPY --from=jitsi-source /prosody-plugins /prosody-plugins
COPY --from=jitsi-source /prosody-plugins-contrib /prosody-plugins-contrib
# Copy pure-Lua libraries that the Jitsi plugins depend on (basexx, net-url, etc.)
# We do NOT copy /usr/local/lib/lua (compiled .so for glibc/Debian — incompatible with musl/Alpine)
# lua5.4-cjson is provided by Alpine's prosody dependency and takes precedence
COPY --from=jitsi-source /usr/local/share/lua/5.4 /usr/local/share/lua/5.4
# Copy rootfs overlay (mirrors Linux FHS)
COPY docker/rootfs/ /
# Copy Dockerfile into image for reference
COPY docker/Dockerfile /root/Dockerfile
# Set ownership and permissions
RUN chown -R prosody:prosody /config /prosody-plugins-custom /prosody-plugins-contrib && \
chmod 755 /usr/local/bin/*
# BOSH + WebSocket (5280), c2s client connections (5222), component connections (5347)
EXPOSE 5222 5280 5347
STOPSIGNAL SIGRTMIN+3
HEALTHCHECK --start-period=2m --interval=1m --timeout=10s --retries=3 \
CMD /usr/local/bin/healthcheck.sh || exit 1
ENTRYPOINT [ "tini", "-p", "SIGTERM", "--", "/usr/local/bin/entrypoint.sh" ]
+29
View File
@@ -0,0 +1,29 @@
services:
prosody:
image: ghcr.io/casjaysdevdocker/prosody:latest
restart: unless-stopped
environment:
- 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
- JICOFO_AUTH_PASSWORD=changeme_jicofo
- JVB_AUTH_PASSWORD=changeme_jvb
- ENABLE_AUTH=0
- ENABLE_GUESTS=0
- ENABLE_XMPP_WEBSOCKET=1
- LOG_LEVEL=info
volumes:
- ./volumes/config:/config
ports:
- "5222:5222"
- "5280:5280"
- "5347:5347"
networks:
- prosody-net
networks:
prosody-net:
driver: bridge
+489
View File
@@ -0,0 +1,489 @@
#!/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
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# healthcheck.sh — container health probe
# Returns 0 (healthy) if prosody's HTTP health endpoint responds 200
: "${PROSODY_HTTP_PORT:=5280}"
curl -q -LSs --max-time 5 "http://127.0.0.1:${PROSODY_HTTP_PORT}/health" -o /dev/null