Skip to content

Instantly share code, notes, and snippets.

@ergolyam
Last active April 20, 2026 23:43
Show Gist options
  • Select an option

  • Save ergolyam/c4b06286748bf2afcfea167777fe230c to your computer and use it in GitHub Desktop.

Select an option

Save ergolyam/c4b06286748bf2afcfea167777fe230c to your computer and use it in GitHub Desktop.
Развёртывание Matrix-сервера со звонками на VPS в podman quadlet

Развёртывание Matrix-сервера со звонками на VPS в podman quadlet

Инструкция поднимает связку Continuwuity + Element Web + LiveKit + lk-jwt-service в podman-контейнерах через quadlet, с Nginx как TLS reverse proxy.

Matrix-сервер и Element Web будут доступны на основном домене, а backend для звонков - на отдельном поддомене.

Везде, где указано example.com и calls.example.com, подставьте свои домены.

Содержание

Требования и проверки перед стартом

Необходимо

  • 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.4
    • systemd версии >= 244

Проверка окружения

  1. Проверьте DNS: оба домена указывают на IP VPS.

    dig +short A example.com
    dig +short A calls.example.com
  2. Убедитесь, что нужные порты не заняты другими сервисами:

    ss -tulpn | grep -E ':(80|443|7881|3478)\b'
  3. Проверьте версии:

    podman --version
    systemctl --version

Подготовка окружения

1. Создайте volumes и каталоги

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

2. Выпустите TLS-сертификаты

Сертификаты сохраняются в volume nginx-certs.

  1. Выберите CA = Let's Encrypt:

    podman run --rm -it -v nginx-certs:/acme.sh docker.io/neilpang/acme.sh \
      --set-default-ca --server letsencrypt
  2. Выпустите сертификат для основного домена:

    podman run --rm -it -p 80:80 -v nginx-certs:/acme.sh docker.io/neilpang/acme.sh \
      --issue --standalone -d example.com
  3. Выпустите сертификат для домена звонков:

    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.

3. Сгенерируйте ключи LiveKit

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 на реальные значения.

4. Создайте конфиг LiveKit

# /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

5. Создайте конфиг Element Web

Файл: /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"
}

Системные настройки и сервисы

6. Создайте главный pod

# /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

7. Создайте сервис Continuwuity

# /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 нужен для регистрации новых пользователей. Не оставляйте значение из примера.

8. Создайте сервис LiveKit

# /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

9. Создайте сервис lk-jwt-service

# /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 должен совпадать с Matrix server_name.

10. Создайте сервис Element Web

# /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

11. Создайте сервис Nginx

# /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

12. Настройте автопродление сертификатов

# /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

Конфигурация Nginx

13. Создайте общий конфиг для WebSocket

# /etc/nginx/conf.d/00-websocket.conf
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

14. Создайте конфиг для Matrix и Element Web

# /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;
    }
}

15. Создайте конфиг для LiveKit и lk-jwt-service

# /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;
    }
}

Запуск и проверка

16. Запустите pod и контейнеры

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'

17. Проверьте Matrix API и well-known

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_url https://example.com.
  • /_matrix/client/versions возвращает JSON со списком поддерживаемых Matrix API.

18. Проверьте MatrixRTC discovery и JWT service

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"
    }
  ]
}

19. Проверьте логи

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

20. Включите таймер продления сертификатов

systemctl enable --now nginx-renew.timer
systemctl list-timers | grep nginx-renew.timer

Подключение клиента

21. Откройте Element Web

  • Откройте в браузере: https://example.com
  • Зарегистрируйте пользователя с токеном из CONDUIT_REGISTRATION_TOKEN.
  • Создайте комнату и проверьте аудио или видеозвонок.

Если регистрация больше не нужна, замените CONDUIT_ALLOW_REGISTRATION=true на false и перезапустите conduit.service.

22. Подключитесь из стороннего Matrix-клиента

  • 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.

1. Включите таймер автообновления

systemctl enable --now podman-auto-update.timer

2. Проверьте расписание

systemctl status podman-auto-update.timer --no-pager
systemctl list-timers | grep podman-auto-update

Практические замечания

  • CONDUIT_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. Лучше сначала убедиться, что звонки работают, и только затем изучать отдельный режим с запретом удалённых серверов.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment