{"id":3289,"date":"2026-06-02T03:23:09","date_gmt":"2026-06-02T03:23:09","guid":{"rendered":"https:\/\/muhammadsoliman.com\/?p=3289"},"modified":"2026-06-02T03:53:20","modified_gmt":"2026-06-02T03:53:20","slug":"how-we-built-a-self-hosted-jitsi-meet-server-on-proxmox-with-traefik-cloudflare-ssl-udp-media-and-turn","status":"publish","type":"post","link":"https:\/\/muhammadsoliman.com\/index.php\/2026\/06\/02\/how-we-built-a-self-hosted-jitsi-meet-server-on-proxmox-with-traefik-cloudflare-ssl-udp-media-and-turn\/","title":{"rendered":"How We Built a Self-Hosted Jitsi Meet Server on Proxmox with Traefik, Cloudflare SSL, UDP Media, and TURN"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">Build Jitsi video Conference on VM <\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">Overview<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In this guide, we will build a production-style self-hosted Jitsi Meet setup for online classes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The final public meeting URL will be:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:&#47;&#47;meet.example.com\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The design uses separate virtual machines:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>VM 100 \u2014 Docker Gateway \/ Traefik \/ Portainer\nVM 101 \u2014 Jitsi Meet only\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Traefik handles the secure HTTPS web route, while Jitsi media traffic goes directly to the Jitsi VM using UDP <code>10000<\/code>. TURN\/STUN is also configured for mobile networks and restrictive connections.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This separation is important because Jitsi real-time media traffic should not be treated like a normal web app. Traefik handles HTTP\/HTTPS, but Jitsi media needs direct UDP routing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Official Jitsi Docker documentation also states that for a real deployment, <code>PUBLIC_URL<\/code> must be set, and that HTTP on port <code>8000<\/code> is useful behind a reverse proxy. (<a href=\"https:\/\/jitsi.github.io\/handbook\/docs\/devops-guide\/devops-guide-docker\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">jitsi.github.io<\/a>)<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n<style>.kb-row-layout-id3289_6b1a8b-95 > .kt-row-column-wrap{align-content:start;}:where(.kb-row-layout-id3289_6b1a8b-95 > .kt-row-column-wrap) > .wp-block-kadence-column{justify-content:start;}.kb-row-layout-id3289_6b1a8b-95 > .kt-row-column-wrap{column-gap:var(--global-kb-gap-md, 2rem);row-gap:var(--global-kb-gap-md, 2rem);padding-top:var(--global-kb-spacing-sm, 1.5rem);padding-bottom:var(--global-kb-spacing-sm, 1.5rem);grid-template-columns:minmax(0, 1fr);}.kb-row-layout-id3289_6b1a8b-95 > .kt-row-layout-overlay{opacity:0.30;}@media all and (max-width: 1024px){.kb-row-layout-id3289_6b1a8b-95 > .kt-row-column-wrap{grid-template-columns:minmax(0, 1fr);}}@media all and (max-width: 767px){.kb-row-layout-id3289_6b1a8b-95 > .kt-row-column-wrap{grid-template-columns:minmax(0, 1fr);}}<\/style><div class=\"kb-row-layout-wrap kb-row-layout-id3289_6b1a8b-95 alignnone wp-block-kadence-rowlayout\"><div class=\"kt-row-column-wrap kt-has-1-columns kt-row-layout-equal kt-tab-layout-inherit kt-mobile-layout-row kt-row-valign-top\">\n<style>.kadence-column3289_d49006-ef > .kt-inside-inner-col,.kadence-column3289_d49006-ef > .kt-inside-inner-col:before{border-top-left-radius:0px;border-top-right-radius:0px;border-bottom-right-radius:0px;border-bottom-left-radius:0px;}.kadence-column3289_d49006-ef > .kt-inside-inner-col{column-gap:var(--global-kb-gap-sm, 1rem);}.kadence-column3289_d49006-ef > .kt-inside-inner-col{flex-direction:column;}.kadence-column3289_d49006-ef > .kt-inside-inner-col > .aligncenter{width:100%;}.kadence-column3289_d49006-ef > .kt-inside-inner-col:before{opacity:0.3;}.kadence-column3289_d49006-ef{position:relative;}@media all and (max-width: 1024px){.kadence-column3289_d49006-ef > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}@media all and (max-width: 767px){.kadence-column3289_d49006-ef > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}<\/style>\n<div class=\"wp-block-kadence-column kadence-column3289_d49006-ef\"><div class=\"kt-inside-inner-col\"><style>.kb-image3289_73a0cd-7d .kb-image-has-overlay:after{opacity:0.3;}<\/style>\n<figure class=\"wp-block-kadence-image kb-image3289_73a0cd-7d size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1000\" height=\"1000\" src=\"http:\/\/muhammadsoliman.com\/wp-content\/uploads\/2026\/06\/1000x1000.png\" alt=\"\" class=\"kb-img wp-image-3302\" srcset=\"https:\/\/muhammadsoliman.com\/wp-content\/uploads\/2026\/06\/1000x1000.png 1000w, https:\/\/muhammadsoliman.com\/wp-content\/uploads\/2026\/06\/1000x1000-300x300.png 300w, https:\/\/muhammadsoliman.com\/wp-content\/uploads\/2026\/06\/1000x1000-150x150.png 150w, https:\/\/muhammadsoliman.com\/wp-content\/uploads\/2026\/06\/1000x1000-768x768.png 768w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n<\/div><\/div>\n\n<\/div><\/div>\n\n\n<h1 class=\"wp-block-heading\">Final Architecture<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">Physical Server<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>Dedicated server\nProxmox VE installed\nZFS RAID1 mirror\nPublic network bridge: vmbr0\nPrivate VM bridge: vmbr1\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Private Network<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>vmbr1 gateway: 10.10.100.1\/24\nVM 100 Docker\/Traefik: 10.10.100.100\nVM 101 Jitsi Meet: 10.10.100.101\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Public Traffic Flow<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Web traffic<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>Browser\n\u2192 https:\/\/meet.example.com\n\u2192 Cloudflare DNS-only A record\n\u2192 Server public IP\n\u2192 Hetzner\/firewall TCP 443\n\u2192 Proxmox DNAT TCP 443\n\u2192 VM 100 Traefik\n\u2192 VM 101 Jitsi web backend on http:\/\/10.10.100.101:8000\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Jitsi media traffic<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>Browser \/ app\n\u2192 UDP 10000\n\u2192 Server public IP\n\u2192 Proxmox DNAT\n\u2192 VM 101 Jitsi Videobridge\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">TURN\/STUN traffic<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>Client\n\u2192 UDP 3478\n\u2192 Server public IP\n\u2192 Proxmox DNAT\n\u2192 VM 101 coturn\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Ports Used<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">Public ports<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>TCP 80     \u2192 VM 100 Traefik\nTCP 443    \u2192 VM 100 Traefik\nUDP 10000  \u2192 VM 101 Jitsi Videobridge\nUDP 3478   \u2192 VM 101 coturn TURN\/STUN\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Optional future ports:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TCP 5349   \u2192 TURN TLS fallback\nUDP 49160-49220 \u2192 TURN relay range, if configured\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Do not forward public TCP <code>80<\/code> or <code>443<\/code> directly to the Jitsi VM in this design. Traefik already owns public web traffic.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">DNS Records<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Create DNS-only records, not proxied\/orange-clouded:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>A  meet.example.com  \u2192 &#91;SERVER_PUBLIC_IPV4]\nA  turn.example.com  \u2192 &#91;SERVER_PUBLIC_IPV4]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For Cloudflare, keep them as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Proxy status: DNS only\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This matters because WebRTC media and TURN traffic should not be proxied through Cloudflare.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">VM 101 Specification<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Recommended Jitsi VM:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>VM ID: 101\nName: jitsi-meet\nOS: Debian 12\nCPU: 6\u20138 vCPU\nRAM: 16 GB\nDisk: 120\u2013200 GB\nNetwork: vmbr1\nIP: 10.10.100.101\/24\nGateway: 10.10.100.1\nDNS: 1.1.1.1, 8.8.8.8\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Official Jitsi requirements are lower for small setups, but we intentionally use more resources for reliability in live classes. Jitsi\u2019s requirements page also warns that extra services such as recording increase resource needs. (<a href=\"https:\/\/jitsi.github.io\/handbook\/docs\/devops-guide\/devops-guide-requirements\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">jitsi.github.io<\/a>)<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 1 \u2014 Prepare VM 101<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Install a clean Debian 12 VM.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After installation, configure:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>IP address: 10.10.100.101\/24\nGateway: 10.10.100.1\nDNS: 1.1.1.1 and 8.8.8.8\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Install basic tools:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update\nsudo apt install -y sudo openssh-server curl ca-certificates gnupg qemu-guest-agent nano htop\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Enable and start the guest agent:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl enable --now qemu-guest-agent\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Verify networking:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ping -c 3 10.10.100.1\nping -c 3 1.1.1.1\nping -c 3 deb.debian.org\nsudo apt update\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Success means the VM can reach the private gateway, the internet, DNS, and Debian repositories.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 2 \u2014 Install Docker on VM 101<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Install Docker Engine and Docker Compose plugin:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update\nsudo apt install -y ca-certificates curl gnupg\n\nsudo install -m 0755 -d \/etc\/apt\/keyrings\n\ncurl -fsSL https:\/\/download.docker.com\/linux\/debian\/gpg \\\n  | sudo gpg --dearmor -o \/etc\/apt\/keyrings\/docker.gpg\n\nsudo chmod a+r \/etc\/apt\/keyrings\/docker.gpg\n\necho \\\n  \"deb &#91;arch=$(dpkg --print-architecture) signed-by=\/etc\/apt\/keyrings\/docker.gpg] https:\/\/download.docker.com\/linux\/debian \\\n  $(. \/etc\/os-release &amp;&amp; echo \"$VERSION_CODENAME\") stable\" \\\n  | sudo tee \/etc\/apt\/sources.list.d\/docker.list &gt; \/dev\/null\n\nsudo apt update\n\nsudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Add the admin user to the Docker group:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo usermod -aG docker $USER\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Log out and back in, then test:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker run hello-world\ndocker compose version\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 3 \u2014 Download Official Jitsi Docker Release<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Do not clone the development GitHub repo for production release use.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use the official Docker release package:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo mkdir -p \/opt\/jitsi\nsudo chown -R \"$USER:$USER\" \/opt\/jitsi\n\ncd \/opt\/jitsi\n\nwget \"$(wget -q -O - https:\/\/api.github.com\/repos\/jitsi\/docker-jitsi-meet\/releases\/latest | grep zip | cut -d\\\" -f4)\"\n\nunzip stable-*.zip\n\ncd docker-jitsi-meet\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Official Jitsi Docker documentation says to download the latest release, copy <code>env.example<\/code> to <code>.env<\/code>, run <code>gen-passwords.sh<\/code>, create the config directories, and then run Docker Compose. (<a href=\"https:\/\/jitsi.github.io\/handbook\/docs\/devops-guide\/devops-guide-docker\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">jitsi.github.io<\/a>)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Copy the environment file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cp env.example .env\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Generate strong passwords:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.\/gen-passwords.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create config folders:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p ~\/.jitsi-meet-cfg\/{web,transcripts,prosody\/config,prosody\/prosody-plugins-custom,jicofo,jvb,jigasi,jibri}\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 4 \u2014 Configure Jitsi <code>.env<\/code> for Traefik Reverse Proxy<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Edit <code>.env<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>nano .env\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Use these core settings:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PUBLIC_URL=https:\/\/meet.example.com\n\nENABLE_LETSENCRYPT=0\nDISABLE_HTTPS=1\nENABLE_HTTP_REDIRECT=0\n\nHTTP_PORT=8000\nHTTPS_PORT=8443\n\nJVB_PORT=10000\nJVB_ADVERTISE_IPS=&#91;SERVER_PUBLIC_IPV4]\n\nENABLE_P2P=false\n\nRESOLUTION=360\nRESOLUTION_MIN=180\nRESOLUTION_WIDTH=640\nRESOLUTION_WIDTH_MIN=320\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Explanation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PUBLIC_URL tells Jitsi its real public URL.\nDISABLE_HTTPS=1 lets Traefik handle HTTPS.\nENABLE_LETSENCRYPT=0 disables Jitsi\u2019s own cert handling.\nHTTP_PORT=8000 gives Traefik a private HTTP backend.\nJVB_ADVERTISE_IPS tells clients the real public IP for media.\nENABLE_P2P=false keeps all media through JVB, which is better for future recording bots.\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Jitsi\u2019s Docker guide specifically notes that HTTP port <code>8000<\/code> is available for reverse proxy setups and that <code>PUBLIC_URL<\/code> must be set for real deployments. (<a href=\"https:\/\/jitsi.github.io\/handbook\/docs\/devops-guide\/devops-guide-docker\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">jitsi.github.io<\/a>)<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 5 \u2014 Start Jitsi<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">From:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd \/opt\/jitsi\/docker-jitsi-meet\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Start the containers:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose pull\ndocker compose up -d\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Check containers:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose ps\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected containers:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>web\nprosody\njicofo\njvb\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected ports:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>web: TCP 8000 and 8443\njvb: UDP 10000\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Test locally on VM 101:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -I http:\/\/127.0.0.1:8000\ncurl -I http:\/\/10.10.100.101:8000\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected result:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>HTTP\/1.1 200 OK\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 6 \u2014 Add Traefik Route on VM 100<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">On the Docker\/Traefik VM, edit:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/opt\/docker\/traefik\/data\/config.yml\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Add a Jitsi router and service:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>http:\n  routers:\n    jitsi-meet:\n      entryPoints:\n        - \"https\"\n      rule: \"Host(`meet.example.com`)\"\n      tls:\n        certResolver: cloudflare\n      service: \"jitsi-meet\"\n\n  services:\n    jitsi-meet:\n      loadBalancer:\n        servers:\n          - url: \"http:\/\/10.10.100.101:8000\"\n        passHostHeader: true\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Restart or reload Traefik:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd \/opt\/docker\/traefik\ndocker compose restart traefik\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Test from VM 100:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -k -I --resolve meet.example.com:443:127.0.0.1 https:\/\/meet.example.com\/\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>HTTP\/2 200\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 7 \u2014 Configure Proxmox DNAT for Jitsi Media<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Traefik handles only web traffic. Jitsi media must go directly to VM 101.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On the Proxmox host, update your NAT script to include:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Jitsi Videobridge media\niptables -t nat -C PREROUTING -i \"$WAN_IF\" -p udp --dport 10000 -j DNAT --to-destination \"$JITSI_VM_IP:10000\" 2&gt;\/dev\/null || \\\niptables -t nat -A PREROUTING -i \"$WAN_IF\" -p udp --dport 10000 -j DNAT --to-destination \"$JITSI_VM_IP:10000\"\n\niptables -C FORWARD -i \"$WAN_IF\" -o \"$LAN_IF\" -p udp -d \"$JITSI_VM_IP\" --dport 10000 -j ACCEPT 2&gt;\/dev\/null || \\\niptables -A FORWARD -i \"$WAN_IF\" -o \"$LAN_IF\" -p udp -d \"$JITSI_VM_IP\" --dport 10000 -j ACCEPT\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Example variables:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>WAN_IF=\"vmbr0\"\nLAN_IF=\"vmbr1\"\nJITSI_VM_IP=\"10.10.100.101\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Restart NAT service:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl restart vmbr1-nat.service\nsystemctl status vmbr1-nat.service --no-pager -l\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Check rules:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>iptables -t nat -L PREROUTING -v -n --line-numbers | grep 10000\niptables -L FORWARD -v -n --line-numbers | grep 10000\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 8 \u2014 Open Firewall Ports<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">At the provider firewall, allow:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TCP 80\nTCP 443\nUDP 10000\nUDP 3478\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Optional later:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TCP 5349\nUDP TURN relay range\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For this setup, the critical Jitsi media port is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>UDP 10000\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the page loads but audio\/video fails, check UDP <code>10000<\/code> first.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 9 \u2014 Test Jitsi Web and Media<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Open:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:&#47;&#47;meet.example.com\/test-room-1\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Join from two devices.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In Jitsi connection stats, verify:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Connection: Good or Acceptable\nTransport: udp\nRemote port: 10000\nPacket loss: 0% or low\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If Jitsi web loads but video\/audio drops, check:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>iptables -t nat -L PREROUTING -v -n --line-numbers | grep 10000\niptables -L FORWARD -v -n --line-numbers | grep 10000\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Counters should increase during a meeting.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 10 \u2014 Install coturn on VM 101<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Install coturn:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update\nsudo apt install -y coturn openssl\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Enable coturn:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo sed -i 's\/^#TURNSERVER_ENABLED=.*\/TURNSERVER_ENABLED=1\/' \/etc\/default\/coturn\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Generate a TURN secret:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>openssl rand -hex 32 &gt; \/home\/jitsiadmin\/.turn_secret\nchmod 600 \/home\/jitsiadmin\/.turn_secret\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Edit coturn config:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/etc\/turnserver.conf\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>listening-port=3478\nfingerprint\nuse-auth-secret\nstatic-auth-secret=&#91;TURN_SECRET]\nrealm=turn.example.com\nserver-name=turn.example.com\n\nlistening-ip=10.10.100.101\nrelay-ip=10.10.100.101\nexternal-ip=&#91;SERVER_PUBLIC_IPV4]\/10.10.100.101\n\nmin-port=49160\nmax-port=49220\n\nno-tls\nno-dtls\nno-cli\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Start coturn:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl enable --now coturn\nsudo systemctl status coturn --no-pager -l\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Verify listening ports:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ss -lntup | grep 3478\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>UDP 3478\nTCP 3478\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 11 \u2014 Add TURN DNAT on Proxmox<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Add NAT forwarding for TURN:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># TURN\/STUN UDP 3478\niptables -t nat -C PREROUTING -i \"$WAN_IF\" -p udp --dport 3478 -j DNAT --to-destination \"$JITSI_VM_IP:3478\" 2&gt;\/dev\/null || \\\niptables -t nat -A PREROUTING -i \"$WAN_IF\" -p udp --dport 3478 -j DNAT --to-destination \"$JITSI_VM_IP:3478\"\n\niptables -C FORWARD -i \"$WAN_IF\" -o \"$LAN_IF\" -p udp -d \"$JITSI_VM_IP\" --dport 3478 -j ACCEPT 2&gt;\/dev\/null || \\\niptables -A FORWARD -i \"$WAN_IF\" -o \"$LAN_IF\" -p udp -d \"$JITSI_VM_IP\" --dport 3478 -j ACCEPT\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Optional relay range:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>iptables -t nat -C PREROUTING -i \"$WAN_IF\" -p udp --dport 49160:49220 -j DNAT --to-destination \"$JITSI_VM_IP\" 2&gt;\/dev\/null || \\\niptables -t nat -A PREROUTING -i \"$WAN_IF\" -p udp --dport 49160:49220 -j DNAT --to-destination \"$JITSI_VM_IP\"\n\niptables -C FORWARD -i \"$WAN_IF\" -o \"$LAN_IF\" -p udp -d \"$JITSI_VM_IP\" --dport 49160:49220 -j ACCEPT 2&gt;\/dev\/null || \\\niptables -A FORWARD -i \"$WAN_IF\" -o \"$LAN_IF\" -p udp -d \"$JITSI_VM_IP\" --dport 49160:49220 -j ACCEPT\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Restart NAT service:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl restart vmbr1-nat.service\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 12 \u2014 Advertise TURN in Jitsi<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">On VM 101:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd \/opt\/jitsi\/docker-jitsi-meet\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Add TURN settings to <code>.env<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TURN_HOST=turn.example.com\nTURN_PORT=3478\nTURN_TRANSPORT=udp\nTURN_CREDENTIALS=&#91;TURN_SECRET]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Keep:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ENABLE_P2P=false\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Recreate Jitsi containers:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose up -d --force-recreate\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Verify Prosody generated TURN config:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose exec -T prosody sh -lc '\ngrep -nEi \"external_services|turn.example.com|3478|external_service_secret\" \/config\/prosody.cfg.lua 2&gt;\/dev\/null \\\n  | sed -E \"s\/(external_service_secret = ).*\/\\1&#91;HIDDEN];\/\"\n'\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>external_services\nturn.example.com\nport = 3478\nexternal_service_secret = &#91;HIDDEN]\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 13 \u2014 Test on Wi-Fi and Mobile Data<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Test:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Desktop\/laptop on Wi-Fi\nPhone on 5G\/mobile data\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Open:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:&#47;&#47;meet.example.com\/test-turn-5g-1\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Watch counters on Proxmox:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>iptables -t nat -L PREROUTING -v -n --line-numbers | grep -E '10000|3478|49160|49220|10.10.100.101'\niptables -L FORWARD -v -n --line-numbers | grep -E '10000|3478|49160|49220|10.10.100.101'\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A good result:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Connection: Good\nPacket loss: 0%\nAudio works\nVideo works\nNo disconnect\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In our test, the phone on low 5G worked well in the mobile browser with audio\/video and no disconnect.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 14 \u2014 Recommended Quality Settings<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For low-internet online classes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ENABLE_P2P=false\nRESOLUTION=360\nRESOLUTION_MIN=180\nRESOLUTION_WIDTH=640\nRESOLUTION_WIDTH_MIN=320\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This gives:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Preferred: 360p\nMinimum: 180p\nAudio priority\nServer-routed media through JVB\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Recommended classroom policy:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Default\/group\/recorded classes:\n360p default, 480p max later, audio priority, P2P off\n\nPure 1-to-1 non-recorded classes:\nOptional 720p profile later, only through a classroom wrapper app\n\nRecorder-bot or 3+ participant classes:\nNo 720p, use JVB routing, P2P off\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 15 \u2014 Branding Cleanup<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For a quick branding improvement, hide the Jitsi watermark and add text branding such as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Ulamaify Meet\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The simple temporary approach is CSS injection into Jitsi\u2019s web assets. This is not the final permanent branding method, but it works for a clean launch.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Final branding should later happen in:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>classroom.example.com\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That wrapper page can show your logo, class name, recording notice, and then embed or launch the Jitsi room.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 16 \u2014 Recording Decision<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Do not install Jibri on the Jitsi VM.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Official Jitsi requirements say Jibri needs one system per recording, uses much higher CPU\/RAM\/disk, and is not recommended on the same server as Jitsi Meet because it can harm performance and exhaust disk. (<a href=\"https:\/\/jitsi.github.io\/handbook\/docs\/devops-guide\/devops-guide-requirements\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">jitsi.github.io<\/a>)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Temporary recording options:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Jitsi local recording:\nWorks, saves .webm locally, but files can be large and it is hard for teachers.\n\nOBS:\nWorks, but teacher workflow is still too complicated.\n\nBest temporary decision:\nDo not require teachers to record manually.\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Official Jitsi FAQ confirms local recording saves a WebM file on the device. (<a href=\"https:\/\/jitsi.github.io\/handbook\/docs\/faq\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">jitsi.github.io<\/a>)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Final production recording plan:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Separate Recorder Bot VM\nBot joins as third participant\nRecords audio\nTakes snapshots every 3\u20135 minutes\nUploads files\nn8n handles transcription\/report generation later\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Do not put recording bots, Jibri, n8n, transcription, databases, or AI workloads on the Jitsi VM.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 17 \u2014 Mobile App Policy<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">In testing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Jitsi app on Wi-Fi: better\nJitsi app on 5G\/mobile data: poor audio\/video\nMobile browser on 5G: better\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Recommended student policy:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Desktop\/laptop:\nUse Chrome or Edge browser.\n\nPhone\/tablet on Wi-Fi:\nUse app or browser.\n\nPhone\/tablet on mobile data\/5G:\nUse browser first.\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Later app troubleshooting can test:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Jitsi app on 5G while watching UDP 10000 \/ UDP 3478 counters\nApp connection stats screenshots\nTURN TCP\/TLS 5349\nPossibly TURN-over-443, but only with careful design because Traefik already uses TCP 443\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Step 18 \u2014 Snapshots<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">After the setup works, create snapshots.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">VM 101:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>qm snapshot 101 jitsi-turn-5g-working \\\n  --description \"Jitsi working with P2P disabled, 360p profile, UDP 10000 media, coturn UDP 3478 advertised, phone 5G browser test successful\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">VM 100:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>qm snapshot 100 docker-traefik-jitsi-route-clean \\\n  --description \"Traefik clean Jitsi route: meet.example.com only, old domains removed, route returns HTTP\/2 200\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Snapshots are good rollback points, but they are not a replacement for off-server backups.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Final Working Checklist<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">A working deployment should have:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>meet.example.com opens publicly\nTraefik route returns HTTP\/2 200\nOld temporary domains return 404\nJitsi containers are Up\nLocal Jitsi web returns HTTP\/1.1 200 OK\nUDP 10000 reaches VM 101\ncoturn is active\nUDP 3478 reaches VM 101\nPhone browser on 5G works with no disconnect\nP2P is disabled\n360p profile is active\nSnapshots are created\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Useful checks:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose ps\ncurl -I http:\/\/127.0.0.1:8000\nsudo ss -lntup | grep -E ':8000|:8443|:10000|:3478'\niptables -t nat -L PREROUTING -v -n --line-numbers | grep -E '10000|3478'\niptables -L FORWARD -v -n --line-numbers | grep -E '10000|3478'\ncurl -k -I https:\/\/meet.example.com\/\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Final Summary <\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This setup gives you a clean, separated Jitsi Meet deployment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>VM 100 handles HTTPS and routing with Traefik.\nVM 101 runs only Jitsi and TURN.\nUDP 10000 is forwarded directly for media.\nUDP 3478 is used for TURN\/STUN.\nVideo is conservative at 360p for low-internet users.\nP2P is disabled to prepare for future recorder-bot classes.\nRecording is intentionally postponed until a dedicated server-side Recorder Bot VM is built.\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Build Jitsi video Conference on VM Overview In this guide, we will build a production-style self-hosted&#8230;<\/p>\n","protected":false},"author":1,"featured_media":3302,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_kad_blocks_custom_css":"","_kad_blocks_head_custom_js":"","_kad_blocks_body_custom_js":"","_kad_blocks_footer_custom_js":"","_kadence_starter_templates_imported_post":false,"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"show","_kad_post_feature_position":"above","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"categories":[1],"tags":[],"class_list":["post-3289","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"taxonomy_info":{"category":[{"value":1,"label":"Uncategorized"}]},"featured_image_src_large":["https:\/\/muhammadsoliman.com\/wp-content\/uploads\/2026\/06\/1000x1000.png",1000,1000,false],"author_info":{"display_name":"Muhammad Soliman","author_link":"https:\/\/muhammadsoliman.com\/author\/muhmmad-soliman\/"},"comment_info":0,"category_info":[{"term_id":1,"name":"Uncategorized","slug":"uncategorized","term_group":0,"term_taxonomy_id":1,"taxonomy":"category","description":"","parent":0,"count":3,"filter":"raw","cat_ID":1,"category_count":3,"category_description":"","cat_name":"Uncategorized","category_nicename":"uncategorized","category_parent":0}],"tag_info":false,"_links":{"self":[{"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/posts\/3289","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/comments?post=3289"}],"version-history":[{"count":3,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/posts\/3289\/revisions"}],"predecessor-version":[{"id":3306,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/posts\/3289\/revisions\/3306"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/media\/3302"}],"wp:attachment":[{"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/media?parent=3289"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/categories?post=3289"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/tags?post=3289"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}