Инструкция поднимает связку Continuwuity + Element Web + LiveKit + lk-jwt-service в podman-контейнерах через quadlet, с Nginx как TLS reverse proxy.
Matrix-сервер и Element Web будут доступны на основном домене, а backend для звонков - на отдельном поддомене.
Везде, где указано
example.comиcalls.example.com, подставьте свои домены.
- Требования и проверки перед стартом
- Подготовка окружения
- Системные настройки и сервисы
- Конфигурация Nginx
- Запуск и проверка
- Подключение клиента
- Автообновление контейнеров
- Практические замечания
- VPS с белым IPv4 и доступом по SSH (root или sudo).
- Два DNS-имени:
example.com- Matrix homeserver и Element Web.calls.example.com- LiveKit иlk-jwt-service.
- Открытые порты:
80/tcp- для выпуска и продления TLS-сертификатов.443/tcp- Matrix API, Element Web, LiveKit WebSocket/API.7881/tcp- LiveKit RTC over TCP.3478/udp- встроенный TURN LiveKit.50100-50200/udp- LiveKit RTC media.50300-50400/udp- relay-порты встроенного TURN LiveKit.
- На VPS установлены:
podmanверсии>= 4.4systemdверсии>= 244
-
Проверьте DNS: оба домена указывают на IP VPS.
dig +short A example.com dig +short A calls.example.com
-
Убедитесь, что нужные порты не заняты другими сервисами:
ss -tulpn | grep -E ':(80|443|7881|3478)\b'
-
Проверьте версии:
podman --version systemctl --version
podman volume create nginx-certs
podman volume create conduit-db
mkdir -p /etc/containers/systemd
mkdir -p /etc/nginx/conf.d
mkdir -p /etc/element-web
mkdir -p /etc/livekitСертификаты сохраняются в volume nginx-certs.
-
Выберите CA = Let's Encrypt:
podman run --rm -it -v nginx-certs:/acme.sh docker.io/neilpang/acme.sh \ --set-default-ca --server letsencrypt
-
Выпустите сертификат для основного домена:
podman run --rm -it -p 80:80 -v nginx-certs:/acme.sh docker.io/neilpang/acme.sh \ --issue --standalone -d example.com
-
Выпустите сертификат для домена звонков:
podman run --rm -it -p 80:80 -v nginx-certs:/acme.sh docker.io/neilpang/acme.sh \ --issue --standalone -d calls.example.com
После успешного выпуска появятся файлы вида
/acme.sh/example.com_ecc/fullchain.cerи/acme.sh/calls.example.com_ecc/fullchain.cer.
podman run --rm docker.io/livekit/livekit-server:latest generate-keysКоманда вернёт значения:
API Key: LK_MATRIX_KEY
API Secret: LK_MATRIX_SECRET
Ниже замените
LK_MATRIX_KEYиLK_MATRIX_SECRETна реальные значения.
# /etc/livekit/livekit.yaml
port: 7880
bind_addresses:
- ""
rtc:
tcp_port: 7881
port_range_start: 50100
port_range_end: 50200
use_external_ip: true
enable_loopback_candidate: false
turn:
enabled: true
udp_port: 3478
relay_range_start: 50300
relay_range_end: 50400
domain: calls.example.com
keys:
LK_MATRIX_KEY: LK_MATRIX_SECRETФайл: /etc/element-web/config.json
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://example.com",
"server_name": "example.com"
},
"m.identity_server": {
"base_url": "https://vector.im"
}
},
"disable_custom_urls": false,
"disable_guests": false,
"disable_login_language_selector": false,
"disable_3pid_login": false,
"force_verification": false,
"brand": "Element",
"integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api",
"integrations_widgets_urls": [
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
],
"default_widget_container_height": 280,
"default_country_code": "GB",
"show_labs_settings": false,
"features": {
"feature_video_rooms": true,
"feature_group_calls": true,
"feature_element_call_video_rooms": true
},
"default_federate": true,
"room_directory": {
"servers": ["example.com"]
},
"enable_presence_by_hs_url": {
"https://example.com": false,
},
"setting_defaults": {
"breadcrumbs": true
},
"element_call": {
"url": "https://call.example.com",
"brand": "Element Call",
"use_exclusively": true
},
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
}# /etc/containers/systemd/main.pod
[Unit]
After=network-online.target
[Pod]
PublishPort=0.0.0.0:443:443/tcp
PublishPort=0.0.0.0:7881:7881/tcp
PublishPort=0.0.0.0:3478:3478/udp
PublishPort=0.0.0.0:50100-50200:50100-50200/udp
PublishPort=0.0.0.0:50300-50400:50300-50400/udp
[Install]
WantedBy=default.target# /etc/containers/systemd/conduit.container
[Unit]
PartOf=main-pod.service
[Container]
Environment=CONDUIT_SERVER_NAME=example.com
Environment=CONDUIT_WELL_KNOWN__CLIENT=https://example.com
Environment=CONDUIT_WELL_KNOWN__SERVER=example.com:443
Environment=CONDUIT_ADDRESS=0.0.0.0
Environment=CONDUIT_PORT=8008
Environment=CONDUIT_DATABASE_PATH=/var/lib/conduit
Environment=CONDUIT_ALLOW_REGISTRATION=true
Environment=CONDUIT_REGISTRATION_TOKEN=replace_me_with_long_random_token
Environment=CONDUIT_NEW_USER_DISPLAYNAME_SUFFIX=""
Environment=CONDUIT_ALLOW_FEDERATION=true
Environment=CONDUIT_MATRIX_RTC__FOCI=[{type='livekit',livekit_service_url='https://calls.example.com'}]
Image=ghcr.io/continuwuity/continuwuity:latest
AutoUpdate=registry
Pod=main.pod
Volume=conduit-db:/var/lib/conduit
[Service]
Restart=always
CONDUIT_REGISTRATION_TOKENнужен для регистрации новых пользователей. Не оставляйте значение из примера.
# /etc/containers/systemd/livekit.container
[Unit]
PartOf=main-pod.service
[Container]
Image=docker.io/livekit/livekit-server:latest
Exec=--config /etc/livekit.yaml
AutoUpdate=registry
Pod=main.pod
Volume=/etc/livekit/livekit.yaml:/etc/livekit.yaml:ro
[Service]
Restart=always# /etc/containers/systemd/livekit-jwt.container
[Unit]
PartOf=main-pod.service
[Container]
Environment=LIVEKIT_JWT_BIND=:8081
Environment=LIVEKIT_URL=wss://calls.example.com
Environment=LIVEKIT_FULL_ACCESS_HOMESERVERS=example.com
Environment=LIVEKIT_KEY=LK_MATRIX_KEY
Environment=LIVEKIT_SECRET=LK_MATRIX_SECRET
Image=ghcr.io/element-hq/lk-jwt-service:latest
AutoUpdate=registry
Pod=main.pod
[Service]
Restart=always
LIVEKIT_FULL_ACCESS_HOMESERVERSдолжен совпадать с Matrixserver_name.
# /etc/containers/systemd/element-web.container
[Unit]
PartOf=main-pod.service
[Container]
Environment=ELEMENT_WEB_PORT=8009
Image=docker.io/vectorim/element-web:latest
AutoUpdate=registry
Pod=main.pod
Volume=/etc/element-web/config.json:/app/config.json:ro
[Service]
Restart=always# /etc/containers/systemd/nginx.container
[Unit]
PartOf=main-pod.service
[Container]
Image=docker.io/nginx:alpine
AutoUpdate=registry
Pod=main.pod
Volume=/etc/nginx/conf.d:/etc/nginx/conf.d
Volume=nginx-certs:/etc/nginx/certs
[Service]
Restart=always# /etc/containers/systemd/nginx-renew.container
[Container]
Image=docker.io/neilpang/acme.sh:latest
Volume=nginx-certs:/acme.sh
PublishPort=80:80
Exec=--renew-all --standalone
[Service]
Type=oneshot
ExecStartPost=/usr/bin/podman exec systemd-nginx nginx -s reload# /etc/systemd/system/nginx-renew.timer
[Unit]
Description=Run nginx-renew.service
[Timer]
OnCalendar=Mon 03:00:00
RandomizedDelaySec=900
Persistent=true
[Install]
WantedBy=timers.target# /etc/nginx/conf.d/00-websocket.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}# /etc/nginx/conf.d/example.com.conf
server {
listen 443 ssl;
server_name example.com;
http2 on;
client_max_body_size 50M;
ssl_certificate /etc/nginx/certs/example.com_ecc/fullchain.cer;
ssl_certificate_key /etc/nginx/certs/example.com_ecc/example.com.key;
add_header X-Robots-Tag "noindex, nofollow" always;
location ~ ^(/_matrix/|/.well-known/) {
proxy_pass http://127.0.0.1:8008;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:8009;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}# /etc/nginx/conf.d/calls.example.com.conf
server {
listen 443 ssl;
server_name calls.example.com;
http2 on;
ssl_certificate /etc/nginx/certs/calls.example.com_ecc/fullchain.cer;
ssl_certificate_key /etc/nginx/certs/calls.example.com_ecc/calls.example.com.key;
add_header X-Robots-Tag "noindex, nofollow" always;
location ~ ^/(sfu/get|healthz|get_token) {
proxy_pass http://127.0.0.1:8081$request_uri;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
location / {
proxy_pass http://127.0.0.1:7880$request_uri;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
}
}systemctl daemon-reload
systemctl start main-pod.service
systemctl status conduit.service --no-pager
systemctl status element-web.service --no-pager
systemctl status livekit.service --no-pager
systemctl status livekit-jwt.service --no-pager
systemctl status nginx.service --no-pager- Проверьте опубликованные порты:
ss -tulpn | grep -E ':(443|7881|3478)\b'
curl -I https://example.com
curl https://example.com/.well-known/matrix/server
curl https://example.com/.well-known/matrix/client
curl https://example.com/_matrix/client/versionsОжидаемо:
/.well-known/matrix/serverвозвращаетm.serverсо значениемexample.com:443./.well-known/matrix/clientвозвращаетm.homeserverсbase_urlhttps://example.com./_matrix/client/versionsвозвращает JSON со списком поддерживаемых Matrix API.
curl https://example.com/_matrix/client/v1/rtc/transports
curl https://calls.example.com/healthzВ ответе rtc/transports должен быть focus:
{
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://calls.example.com"
}
]
}journalctl -u conduit.service
journalctl -u livekit.service
journalctl -u livekit-jwt.service
journalctl -u nginx.serviceВ логах полезно смотреть на:
- ошибки чтения
/etc/livekit.yaml - ошибки
LIVEKIT_KEYилиLIVEKIT_SECRET - ошибки discovery
MISSING_MATRIX_RTC_FOCUS - ошибки подключения
lk-jwt-serviceкhttps://example.comилиwss://calls.example.com
systemctl enable --now nginx-renew.timer
systemctl list-timers | grep nginx-renew.timer- Откройте в браузере:
https://example.com - Зарегистрируйте пользователя с токеном из
CONDUIT_REGISTRATION_TOKEN. - Создайте комнату и проверьте аудио или видеозвонок.
Если регистрация больше не нужна, замените
CONDUIT_ALLOW_REGISTRATION=trueнаfalseи перезапуститеconduit.service.
- Homeserver URL:
https://example.com - User ID будет иметь вид:
@user:example.com
Для федерации с другими серверами порт
8448/tcpне нужен, потому что/.well-known/matrix/serverуказывает наexample.com:443.
- В quadlet уже задано:
[Container] AutoUpdate=registry
Это позволяет контейнерам автоматически подтягивать новые образы и перезапускаться через
podman-auto-update.
systemctl enable --now podman-auto-update.timersystemctl status podman-auto-update.timer --no-pager
systemctl list-timers | grep podman-auto-updateCONDUIT_SERVER_NAME=example.comнельзя безболезненно поменять после первого запуска с уже созданной базой. Если нужен другой Matrix ID suffix, поменяйте его до старта.- Не публикуйте
7880/tcpнаружу. Этот порт должен быть доступен только Nginx внутри pod. - Если звонок создаётся, но медиа не идёт, сначала проверьте открытые UDP-порты
3478,50100-50200и50300-50400на firewall VPS и у провайдера. - Если
lk-jwt-serviceпишет ошибки при проверке Matrix OpenID токена, проверьте доступностьhttps://example.com/_matrix/client/versionsизнутри pod:podman exec -it systemd-nginx wget -O- https://example.com/_matrix/client/versions - Для небольшого личного сервера диапазонов
50100-50200/udpи50300-50400/udpобычно достаточно. Для большого количества одновременных звонков увеличьте диапазоны вlivekit.yaml,main.podи firewall. - Встроенный TURN в LiveKit работает только для LiveKit. Если вам нужен TURN для старых Matrix VoIP-звонков или других сервисов, поднимайте отдельный
coturnи следите, чтобы его relay-диапазон не пересекался с диапазонами LiveKit. - Если федерация вам не нужна, не выключайте
CONDUIT_ALLOW_FEDERATIONсразу: текущая MatrixRTC связка использует Matrix OpenID/federation paths. Лучше сначала убедиться, что звонки работают, и только затем изучать отдельный режим с запретом удалённых серверов.