Skip to content
All articles
8 min read

How to Start a Bulk SMS Business: The Tech Stack

MD Rakibul Islam RakibMD Rakibul Islam RakibFull-stack developer, DevOps & Linux engineer
How to Start a Bulk SMS Business: The Tech Stack

A bulk SMS business needs tested SMPP routes, a gateway like Jasmin for routing and billing, a customer panel like playSMS, and approved sender IDs.

I set up SMS platforms for people starting this business, mostly in Africa and the USA, and the technical side is the easy part. The projects that struggle are the ones that bought cheap routes without testing them, launched before sender IDs were approved, or had no way to see delivery rates per route. This guide walks through the whole stack the way I build it, layer by layer, so you know what to buy, what to install and what to check before your first customer sends a campaign.

Key takeaways

  • Routes are your product. Your margin and reputation depend on the SMPP routes you buy, so test delivery on real phones before you sell a route.
  • Jasmin is the engine: it connects to your providers, routes each message by country, tracks delivery receipts and bills each customer.
  • playSMS is the shop window: a ready-made web panel where customers log in, top up and send. Build a custom panel only when you need your own brand and payments.
  • Compliance blocks launches. Sender ID registration, US A2P 10DLC and opt-out rules take longer than the server setup.
  • Measure delivery per route from day one. It is the only way to spot a degrading route before customers complain.

How does a bulk SMS platform work?

Every platform I build has the same four layers:

  1. Customers: businesses sending OTPs, alerts and campaigns through your web panel, your HTTP API or an SMPP bind.
  2. Customer panel: accounts, credit, contact lists, scheduled campaigns and reports.
  3. Gateway: the routing engine that picks a route per destination, enforces throughput and collects delivery reports.
  4. Routes: SMPP accounts with aggregators or mobile operators that actually deliver the SMS to phones.

You make money on the difference between what a route costs you per message and what you charge customers, plus setup and sender ID fees if your market allows it.

Layer 1: Buy and test SMS routes

A route is an SMPP account (host, port, system ID, password) from an SMS aggregator or directly from an operator. Before you sign, ask every provider the same questions:

  • Which countries and operators does the route cover, and is it a direct or indirect connection?
  • Do they return real delivery receipts (DLRs), or fake "delivered" for everything?
  • Do they support alphanumeric sender IDs for your target countries, and do those need registration?
  • What throughput (TPS) is allowed per bind, and how many binds?
  • Can you get test credit to measure delivery on your own phones?

Be careful with routes that are much cheaper than the market. Very cheap routes are often "grey" routes that pass traffic through SIM farms or unofficial interconnects. They deliver well for a week, then operators block them, OTPs arrive late and your customers leave. I always start a new business with at least two tested routes per key country, so one can take over when the other degrades.

Layer 2: Jasmin as the gateway

Jasmin is the open-source SMPP gateway I use as the core of these platforms. It connects to each provider as an SMPP client, gives you an HTTP API and an SMPP server for your own customers, and routes every message by destination prefix. Per-user rates and balances let you bill each customer, and delivery receipts flow back to whoever sent the message.

The install and configuration are covered step by step in my guide to installing Jasmin SMS gateway on Ubuntu: connectors, users, routes, DLR callbacks and the firewall rules. For a business, add these on top:

  • One connector per route with submit_throughput set to what the provider allows.
  • Static routes per country prefix, so Bangladesh, Nigeria and the US each go to their best route.
  • Separate groups for OTP customers and marketing customers, so you can send time-critical OTPs over your best route and campaigns over a cheaper one.
  • Resellers on SMPP: bigger customers bind to Jasmin's SMPP server (port 2775) from their own platforms. Whitelist their IPs in the firewall.
Customercampaign playSMSpanel Jasmin credit 100credit 99 Route A+234 · OTP Route B+1 · 10DLC HTTP APIroute by prefix
Customers send from the panel, Jasmin charges their credit and routes each message by country prefix to the route that delivers best there.

Layer 3: The customer panel (playSMS or custom)

playSMS is a free, open-source web interface for SMS gateways and bulk SMS services. It gives you user accounts with credit, phonebooks and groups, bulk and scheduled sending, inbox for replies, a web services API for customers and reports. It supports Jasmin as a gateway; the Jasmin plugin now lives inside the main playSMS project. Customers send in playSMS, playSMS hands each message to Jasmin over its HTTP API, and Jasmin's delivery receipts update the status in the panel.

playSMS is the fastest way to launch: a working panel in a day, which you can theme with your logo. Choose a custom panel when:

  • you want online top-ups with your local payment gateways or Stripe,
  • you sell to developers and need a modern REST API with keys, webhooks and docs,
  • you want your own brand and pricing pages, which also helps you rank on Google.

I build those custom panels with Next.js and NestJS on top of Jasmin, the same stack this website runs on. Jasmin stays the engine either way, so you can start on playSMS and move later without touching your routes.

Layer 4: Compliance and sender IDs

This is the part that delays launches, so start it the same week you buy routes.

  • Sender ID registration: many countries, including several African markets, require alphanumeric sender IDs (like MyBank) to be registered with operators before they deliver. Your route provider tells you which ones and how long it takes.
  • USA: business texts from 10-digit numbers must be registered for A2P 10DLC through The Campaign Registry. Major US carriers block unregistered A2P traffic from long codes, and registration needs a business tax ID (EIN). Toll-free numbers have their own verification process.
  • Consent and opt-out: only send marketing to people who opted in, honour STOP replies, and keep records. Rules differ by country, so check your regulator.
  • Licensing: some countries require a licence to resell SMS or run value-added services. Check with a local lawyer before you invoice customers.

Layer 5: Running it like a real service

  • Server: a hardened Ubuntu 24.04 VPS with a static IP (providers whitelist it). Start with my hardening checklist.
  • Backups: the playSMS database, Jasmin's persisted config in /etc/jasmin/ and your billing records, copied off the server daily. My Linux backup guide shows the setup.
  • Monitoring: alert when a connector unbinds, when the delivery rate on a route drops, and when queues grow in RabbitMQ. Jasmin exposes Prometheus metrics on its HTTP API.
  • Abuse controls: new accounts start with low credit and manual approval, and you check sender IDs and content before big campaigns. One spammer can get your route blocked for everyone.
  • HTTPS everywhere: the panel and API sit behind Nginx with Let's Encrypt; the raw Jasmin ports stay private.

What does it cost to start?

I won't quote numbers that change by country and month, but these are the cost lines to plan for:

  • Server hosting (one VPS is enough to start).
  • Prepaid credit or a monthly commitment with each route provider, charged per message.
  • Sender ID or 10DLC registration fees, where your market charges them.
  • Setup and support for the platform, unless you install and maintain it yourself.
  • A custom panel and payment integration, if you outgrow playSMS.

Launch checklist

  1. Two tested routes per key country, with real DLRs.
  2. Jasmin installed, connectors bound, routes per prefix, persist run.
  3. playSMS (or your panel) connected to Jasmin, test user sending and receiving DLRs.
  4. Sender IDs or 10DLC campaigns approved.
  5. HTTPS, firewall, backups and connector alerts in place.
  6. Terms of service, opt-out handling and an abuse policy published.

Frequently asked questions

Is a bulk SMS business still profitable?

It can be when you have reliable routes at a good price and customers who need SMS, like banks, schools, clinics and e-commerce stores sending OTPs and alerts. Margins come from route quality and service, not from the software, which is open source.

Do I need my own SMS gateway, or can I resell an API?

You can resell another company's API, but you depend on their prices and routes. Running Jasmin lets you connect several providers, switch routes per country and offer SMPP to resellers, which is where most of the value sits.

Is playSMS good enough for a business?

Yes, for launching and for many small and mid-size platforms. Move to a custom panel when you need online payments, a developer-friendly API or your own branded customer experience.

What is SMPP?

SMPP (Short Message Peer-to-Peer) is the standard protocol that SMS gateways and operators use to exchange messages and delivery receipts. Your provider gives you SMPP credentials, and Jasmin uses them to send.

How long does setup take?

The server, Jasmin and playSMS can be running within a few days. Route contracts, testing and sender ID approvals usually take longer, so start them first.

Want your SMS platform built and running?

I set up complete bulk SMS platforms: Jasmin with multiple routes, playSMS or a custom Next.js panel, HTTPS, backups and monitoring, plus support after launch. See my DevOps services or tell me which countries you want to send to.

MD Rakibul Islam Rakib

Written by

MD Rakibul Islam Rakib

Full-stack developer, DevOps engineer and Linux system administrator with 5+ years of production experience. I deploy, harden and fix servers and web apps for clients worldwide, and everything in this article runs on real servers I manage, including this site.

  • bulk SMS business
  • SMS gateway
  • Jasmin
  • playSMS
  • SMPP routes
  • A2P 10DLC
  • sender ID

Keep reading

Self-Host LiveKit on a VPS: Setup, TURN and Tokens
DevOpsOct 7, 2026

Self-Host LiveKit on a VPS: Setup, TURN and Tokens

To self-host LiveKit, point two domains at a VPS, run the livekit/generate Docker image, install what it creates and open the WebRTC UDP port range. LiveKit is the media server I reach for when a client wants video, audio or voice AI inside their own product rather than a ready-made meeting app. It is open source (Apache 2.0), it scales well, and its SDKs cover web, iOS, Android, Flutter and React Native. LiveKit Cloud is the easy hosted option; self-hosting makes sense when you need data on your own servers, a fixed monthly bill, or a region the cloud doesn't serve. This is how I deploy it on a single VPS, plus how it compares with Jitsi, which I have run for clients for years. Key takeaways LiveKit is infrastructure, not an app. You build your own interface with its SDKs, or start from the open-source example meeting app. The generator does most of the work: livekit/generate writes the LiveKit config, Caddy for TLS, Redis and a Docker Compose file. You need two domains: one for LiveKit itself and one for its built-in TURN server, both pointing at the server. Open the UDP range. Media flows over 50000-60000/udp; without it, calls fall back to TCP or TURN and quality suffers. Your backend signs access tokens with the API key and secret. Never put the secret in a browser or mobile app. LiveKit or Jitsi? I get this question on almost every video project, so here is the short version: Pick Jitsi Meet when you want a complete meeting product today: rooms, chat, screen sharing, recording with Jibri and phone dial-in with Jigasi, all in a ready web app. See my Jitsi Meet with JWT and Jibri guide . Pick LiveKit when video is a feature inside your own product: telehealth, online classes in your LMS, live shopping, support calls, or voice AI agents that talk to users in real time. You design the interface; LiveKit moves the media. Both are SFUs: each participant uploads one stream and the server forwards it to the others. Both self-host on Linux with Docker. What you need A VPS with a public IP and no other web server on ports 80 and 443 (Caddy takes them). LiveKit's docs recommend compute-optimised instances, because capacity is limited by CPU and bandwidth, not memory. Two DNS A records: livekit.example.com and livekit-turn.example.com , both pointing to the server IP. Docker on your own machine to run the config generator. A hardened Ubuntu 24.04 server. My hardening checklist covers SSH and firewall basics. Step 1: Generate the configuration Run the generator on your laptop or on the server, in an empty folder: docker pull livekit/generate docker run --rm -it -v$PWD:/output livekit/generate It asks for your two domains and a few options, then writes a folder with: livekit.yaml : the LiveKit server config, including your generated API key and secret, caddy.yaml : Caddy, which gets Let's Encrypt certificates and terminates TLS, redis.conf and docker-compose.yaml , init_script.sh or a cloud_init file that installs everything on a fresh server. Save the API key and secret in your password manager. Your backend needs them to create tokens. Step 2: Install on the server If your provider supports cloud-init, paste the generated cloud-init file into the server's user data when you create it. Otherwise copy the init script to the server and run it: scp init_script.sh root@203.0.113.40: ssh root@203.0.113.40 'chmod +x init_script.sh && ./init_script.sh' The script installs Docker, puts the configuration in /opt/livekit and registers a systemd service called livekit-docker : systemctl status livekit-docker --no-pager cd /opt/livekit && docker compose logs -f livekit LiveKit runs with host networking, which its docs recommend for performance. Keep that in mind for the firewall: the ports are on the host directly. Step 3: Open the firewall sudo ufw allow 80/tcp # certificate issuance sudo ufw allow 443/tcp # HTTPS, WebSocket signalling, TURN/TLS sudo ufw allow 7881/tcp # WebRTC over TCP sudo ufw allow 3478/udp # TURN/UDP sudo ufw allow 50000:60000/udp # WebRTC media Check your cloud provider's firewall or security group too. A closed UDP range is the most common reason a self-hosted LiveKit "connects but shows no video". Each participant uploads one stream and the SFU forwards it to everyone else. Users on strict networks reach it through TURN over port 443, and nobody joins without a token signed by your backend. Step 4: Test it with the LiveKit CLI curl -sSL https://get.livekit.io/cli | bash export LIVEKIT_URL=wss://livekit.example.com export LIVEKIT_API_KEY=APIxxxxxxxx export LIVEKIT_API_SECRET=your-secret lk token create --join --room test-room --identity rakib lk load-test --room test-room --video-publishers 8 The token command prints a JWT you can paste into LiveKit's example web app to join from a browser. The load test adds simulated publishers so you can watch CPU and bandwidth on the server ( htop , nload ) before real users arrive. Raise the publisher count until the server struggles, then size your production server with headroom. Step 5: Issue tokens from your backend In your app, users log in to your backend, which returns a short-lived LiveKit token. With the official Node.js server SDK: import { AccessToken } from "livekit-server-sdk"; export async function livekitToken(userId: string, room: string) { const at = new AccessToken(process.env.LIVEKIT_API_KEY, process.env.LIVEKIT_API_SECRET, { identity: userId, ttl: "1h", }); at.addGrant({ roomJoin: true, room, canPublish: true, canSubscribe: true }); return at.toJwt(); } The grant decides what a user can do. For a webinar, give viewers canPublish: false . Check that the user is allowed in that room before you sign anything; the token is the only lock on the door. Recording, streaming and voice AI Egress records rooms or single tracks to MP4 and object storage, or streams them to RTMP. It is a separate service that needs Redis and plenty of CPU, so I run it on its own server. If you need S3-compatible storage on your own hardware, see my self-hosted S3 alternatives . Ingress brings RTMP or WHIP streams (for example from OBS) into a room. SIP connects phone calls to rooms. Agents: LiveKit's agents framework joins rooms as an AI participant that listens and talks back. Your self-hosted server works with it the same way as LiveKit Cloud. Scaling beyond one server One node handles a lot, but when you need more, add nodes that share the same Redis. LiveKit uses Redis to know which node hosts each room, so a room stays on one node and new rooms spread across the cluster. Put the nodes in the same region as your users; latency matters more than raw server size for real-time media. Troubleshooting Connects, but no audio or video: the UDP range or 7881/tcp is blocked, at the host or at the cloud firewall. Certificate errors: one of the two DNS records doesn't point to the server, or port 80 is closed, so Caddy can't get a certificate. Check docker compose logs caddy . "invalid token" / 401: the token was signed with a different key pair than the one in livekit.yaml , or it expired. Choppy video at scale: CPU or bandwidth is maxed. Use the load test numbers to size up, or add a second node. Frequently asked questions Is LiveKit free to self-host? Yes. The LiveKit server is open source under the Apache 2.0 licence. You pay for your servers and bandwidth; LiveKit Cloud is the paid managed option. Do I need a TURN server for LiveKit? LiveKit has a TURN server built in, and the generated config turns it on with its own domain. It lets users behind strict corporate firewalls connect over port 443. Can LiveKit replace Zoom or Google Meet? It can power your own Zoom-like product, but you build the interface. If you want a finished meeting app without development, self-hosted Jitsi Meet is the faster route. Does self-hosted LiveKit work with voice AI agents? Yes. The agents framework connects to any LiveKit server with a URL, API key and secret, so you can run AI voice assistants against your own deployment. How big a server do I need? It depends on how many people publish video and at what quality. Start with a compute-optimised VPS, run lk load-test with numbers close to your real rooms, and keep headroom. Want LiveKit running for your product? I deploy and maintain self-hosted LiveKit with TURN, Egress recording, token services in your backend, monitoring and scaling, and I build the web app around it. See my DevOps services or tell me what you are building .

Read article →
Install ERPNext 16 on Ubuntu 24.04 for Production
Web DevelopmentOct 7, 2026

Install ERPNext 16 on Ubuntu 24.04 for Production

Install ERPNext 16 on Ubuntu 24.04: add MariaDB 11.8, Python 3.14 (uv) and Node 24, run bench init on version-16, install erpnext, then bench setup production. ERPNext is the open-source ERP I install and customise most for small and mid-size companies, and I have built ERPNext backends for custom mobile apps, including a real-time RFID inventory system with a handheld UHF reader. The most common request I get is not a new feature; it is "our install broke during an update" or "the server is slow". Almost every time, the root cause is the install itself: wrong database settings, the wrong Python, or a development setup running in production. Version 16 (final release on 12 January 2026) raised the requirements again, so here is the clean production install I use now. Key takeaways v16 needs newer tools than Ubuntu 24.04 ships: Python 3.14, Node 24 and MariaDB 11.8. Install them from uv, nvm and the MariaDB repository. Run bench as a normal user (for example frappe ), never as root. Set MariaDB to utf8mb4 before you create the first site, or you will fight encoding errors later. bench start is for development. Production uses bench setup production with Nginx and Supervisor. Back up before every update and test updates on a copy of the site first. What do you need before installing? Server: Ubuntu 24.04 LTS, 4 GB RAM as the minimum. I give real companies 8 GB and 4 vCPU, because background jobs, reports and PDF printing all need memory. Disk: 40 GB or more on SSD. Attachments and backups grow fast. Domain: erp.example.com pointing to the server, for HTTPS. Hardening first: SSH keys, firewall with only 22, 80 and 443 open. My Ubuntu 24.04 hardening checklist covers it. The official requirements for version 16 are Python 3.14, Node.js 24, MariaDB 11.8, Redis 6 or newer, Yarn 1.22+ and wkhtmltopdf 0.12.6 with patched Qt. Check the Frappe installation docs before you start, because they change between major versions. Step 1: Create the frappe user and install packages sudo adduser frappe sudo usermod -aG sudo frappe sudo apt update sudo apt install -y git curl redis-server libmariadb-dev pkg-config \ xvfb libfontconfig nginx supervisor Step 2: Install MariaDB 11.8 and set utf8mb4 Ubuntu 24.04's own MariaDB is the 10.11 series, older than v16 wants, so add MariaDB's official repository for the 11.8 series: curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | \ sudo bash -s -- --mariadb-server-version="mariadb-11.8" sudo apt install -y mariadb-server mariadb-client sudo mariadb-secure-installation Then create /etc/mysql/mariadb.conf.d/99-frappe.cnf : [mysqld] character-set-client-handshake = FALSE character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci [mysql] default-character-set = utf8mb4 sudo systemctl restart mariadb Remember the MariaDB root password. Bench asks for it every time it creates a site. Step 3: Install wkhtmltopdf with patched Qt ERPNext prints invoices and reports to PDF with wkhtmltopdf, and the version in Ubuntu's repository is not the patched-Qt build, which breaks headers and footers. Download the 0.12.6.1 "with patched qt" package for your system from the wkhtmltopdf packaging releases and install it with sudo apt install ./wkhtmltox_*.deb . Confirm with wkhtmltopdf --version ; it should say "with patched qt". Step 4: Python 3.14, Node 24 and bench (as frappe) sudo su - frappe curl -LsSf https://astral.sh/uv/install.sh | sh source ~/.bashrc uv python install 3.14 --default curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash source ~/.bashrc nvm install 24 npm install -g yarn uv tool install frappe-bench bench --version uv keeps Python 3.14 separate from the system Python that Ubuntu's own tools depend on. Never replace /usr/bin/python3 . Step 5: Create the bench, the site and install ERPNext bench init --frappe-branch version-16 frappe-bench cd frappe-bench chmod -R o+rx /home/frappe bench new-site erp.example.com bench get-app --branch version-16 erpnext bench --site erp.example.com install-app erpnext bench --site erp.example.com enable-scheduler bench use erp.example.com Name the site after its domain. With DNS-based multitenancy ( bench config dns_multitenant on ) one bench can serve several companies, each on its own domain and database. Need HR and payroll too? Install the hrms app the same way with --branch version-16 . In production, Nginx serves files and proxies to Gunicorn, Supervisor keeps every Frappe process alive, Redis queues background jobs, and MariaDB holds the data. Step 6: Switch to production (Nginx + Supervisor) sudo env "PATH=$PATH" bench setup production frappe bench setup nginx sudo supervisorctl restart all sudo nginx -t && sudo systemctl reload nginx This writes Nginx and Supervisor configs that run Gunicorn, Socket.IO, the background workers and the scheduler as services that start at boot. If bench setup production complains about a missing tool, install it and rerun; on some setups it needs ansible . Check everything is running with sudo supervisorctl status . Step 7: HTTPS with Let's Encrypt sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d erp.example.com Rerunning bench setup nginx regenerates the Nginx config and can drop the certificate lines certbot added. If HTTPS breaks after a bench change, run certbot again. If renewal ever fails, my guide to fixing Certbot renewal errors walks through the causes. Step 8: Backups and safe updates bench --site erp.example.com backup --with-files ls sites/erp.example.com/private/backups/ Backups on the same server are not backups. Copy the database dump and file archives off-site every night (S3, another server, or rclone to cloud storage), and test a restore on a spare server every few months. My Linux backup guide shows the automation. Before bench update : Take a fresh backup with files. Restore it to a staging copy and run the update there first. Check custom apps, print formats and integrations on staging. Update production in a quiet hour. Common ERPNext install problems Site loads without CSS: assets were not built or Nginx can't read them. Run bench build and check that the home folder permissions allow Nginx to read sites/assets . Emails and reports never run: the scheduler is disabled, or workers are down. Check bench --site erp.example.com doctor and supervisorctl status . PDFs have no header or footer: wkhtmltopdf is not the patched-Qt build. 502 Bad Gateway after a reboot or update: Gunicorn didn't start. Read the Supervisor logs in frappe-bench/logs/ ; my Nginx 502 guide covers the general debugging path. Docker instead? Frappe also ships Docker images (the frappe_docker project and the easy-install.py script). Docker is a good choice when you run many sites or want identical staging and production. For one company on one server, I still prefer the bench install above: fewer layers, simpler debugging, and the community guides match what you see. Frequently asked questions Can ERPNext 16 run on Ubuntu 24.04? Yes. Ubuntu 24.04 works well, but install Python 3.14 with uv, Node 24 with nvm and MariaDB 11.8 from MariaDB's repository, because the Ubuntu packages are older than version 16 requires. How much RAM does ERPNext need? 4 GB is the minimum for a small setup. For a company with daily users, background jobs and PDF printing, I recommend 8 GB, and more if you run several sites on one bench. Is ERPNext free? Yes. ERPNext and the Frappe Framework are open source, so you can self-host without licence fees. You pay for the server, and for setup, customisation and support if you hire help. Should I upgrade from ERPNext 15 to 16? Yes, but on a staging copy first. Check that your custom apps support version 16, update the server tools (Python, Node, MariaDB), then migrate production after testing. Self-hosted ERPNext or Frappe Cloud? Frappe Cloud is managed hosting run by the ERPNext makers, with updates handled for you. Self-hosting gives you full control, custom server-side integrations and data on your own server. Want ERPNext installed, fixed or customised? I install ERPNext in production, upgrade old versions, fix broken benches and build custom apps and mobile integrations on top of it. See my web development services or tell me about your ERPNext setup .

Read article →
Self-Host Jitsi Meet with JWT Auth and Jibri Recording
Linux System AdminOct 7, 2026

Self-Host Jitsi Meet with JWT Auth and Jibri Recording

To self-host Jitsi Meet with JWT and recording, run docker-jitsi-meet on Ubuntu 24.04, set AUTH_TYPE=jwt in .env, open UDP 10000 and add jibri.yml for Jibri. Jitsi Meet is the video conferencing setup I get asked about most after SMS gateways. Schools want private classrooms, clinics want consultations that never touch a third-party cloud, and SaaS teams want video calls inside their own app where only logged-in users can join. My Jitsi install videos (JWT, Jibri, Jigasi and Etherpad on a Linux server) still bring in requests every month. This is the setup I use today: the official Docker images, JWT so your app decides who gets in, and recording that actually works. Key takeaways Use docker-jitsi-meet when you need JWT, Jibri, Etherpad or Jigasi. Each add-on is one extra compose file instead of a manual install. Open UDP 10000 and set JVB_ADVERTISE_IPS to the public IP. If calls work with two people and break with three, this is why. JWT means your app issues the tickets. No token, no meeting, and the token can make someone a moderator. One Jibri records one meeting at a time. Plan Jibri instances for the number of parallel recordings you need. Jigasi needs a SIP account from a VoIP provider for phone dial-in and dial-out. Docker or Debian packages? The Jitsi handbook supports both. The Debian packages ( apt install jitsi-meet , plus jitsi-meet-tokens for JWT) are fine for a plain meeting server on Ubuntu 24.04. Once you add JWT, recording, shared notes and phone dial-in, the Docker setup is easier to reproduce and upgrade: everything lives in one .env file, and each component is a separate container you can restart on its own. I use Docker for client projects for that reason. Server requirements Ubuntu 24.04 with Docker Engine and the Compose plugin. A domain like meet.example.com pointing to the server, for the Let's Encrypt certificate. CPU and bandwidth matter more than RAM for the video bridge. Jibri is heavier: every recording runs a full Chrome browser and ffmpeg, so put it on its own server once you record often. Firewall ports: 80/tcp and 443/tcp for the web and certificates, 10000/udp for media. Step 1: Download the release and generate passwords wget $(wget -q -O - https://api.github.com/repos/jitsi/docker-jitsi-meet/releases/latest | grep zip | cut -d\" -f4) unzip stable-* && cd jitsi-docker-jitsi-meet-* cp env.example .env ./gen-passwords.sh mkdir -p ~/.jitsi-meet-cfg/{web,transcripts,prosody/config,prosody/prosody-plugins-custom,jicofo,jvb,jigasi,jibri} Use a release zip, not a git clone of the main branch. gen-passwords.sh writes strong internal passwords into .env ; the containers refuse to start without them. Step 2: Set the domain, ports and public IP # .env CONFIG=~/.jitsi-meet-cfg HTTP_PORT=80 HTTPS_PORT=443 TZ=Asia/Dhaka PUBLIC_URL=https://meet.example.com ENABLE_LETSENCRYPT=1 LETSENCRYPT_DOMAIN=meet.example.com LETSENCRYPT_EMAIL=admin@example.com JVB_ADVERTISE_IPS=203.0.113.25 JVB_ADVERTISE_IPS is the public IP the video bridge tells browsers to send media to. Get it wrong and the third participant never connects. Then start the base stack and test a meeting: docker compose up -d docker compose ps Step 3: Turn on JWT authentication # .env ENABLE_AUTH=1 AUTH_TYPE=jwt JWT_APP_ID=my_app JWT_APP_SECRET=a-long-random-secret-shared-with-your-backend Restart with docker compose up -d . From now on, a meeting URL only works with a valid token: https://meet.example.com/project-review?jwt=<token> . Your backend signs that token with the shared secret after the user logs in to your app. A minimal Node.js example with the jsonwebtoken package: import jwt from "jsonwebtoken"; const token = jwt.sign( { aud: "jitsi", iss: "my_app", // = JWT_APP_ID sub: "*", // domain or tenant, * for all room: "project-review", // or * for every room context: { user: { name: "Rakib", email: "rakib@example.com", moderator: true } }, }, process.env.JWT_APP_SECRET, { algorithm: "HS256", expiresIn: "2h" }, ); Keep tokens short-lived and per room, so a leaked link stops working. The moderator flag only takes effect when the token_affiliation Prosody module is enabled (for example XMPP_MUC_MODULES=token_affiliation ) and Jicofo's auto-owner is turned off; otherwise the first person to join becomes moderator. Test this with two browsers before you go live, because it is the part that differs most between Jitsi versions. Prosody lets in only users with a valid token from your app. Media goes straight to the video bridge over UDP 10000, and Jibri joins as a hidden participant to record. Step 4: Add recording with Jibri Jibri records by joining the meeting as a hidden participant in Chrome and capturing the screen and audio with ffmpeg. On the host it needs the ALSA loopback kernel module: sudo apt install -y linux-modules-extra-$(uname -r) sudo modprobe snd-aloop echo snd-aloop | sudo tee -a /etc/modules lsmod | grep snd_aloop Some cloud kernels ship without snd-aloop . If modprobe fails, switch the server to the generic kernel, or pick a provider whose image includes it, before you go further. Then enable recording and start the extra container: # .env ENABLE_RECORDING=1 docker compose -f docker-compose.yml -f jibri.yml up -d Recordings land in the Jibri storage folder under your CONFIG directory as MP4 files. Move them to object storage on a schedule, or the disk fills up; I cover cleanup in fixing "No space left on device" . One Jibri container handles one recording or live stream at a time, so add more Jibri instances (ideally on separate servers) for parallel recordings. Step 5: Shared notes with Etherpad # .env ETHERPAD_URL_BASE=http://etherpad.meet.jitsi:9001 docker compose -f docker-compose.yml -f etherpad.yml up -d A "Shared document" button appears in meetings, and everyone in the room edits the same pad. The pad URL is internal to the Docker network, so you don't open any new ports. Step 6: Phone dial-in and dial-out with Jigasi Jigasi is the SIP gateway that lets people join by phone, or lets the meeting call a phone number. You need a SIP account from a VoIP provider: # .env JIGASI_SIP_URI=meet@sip.provider.example JIGASI_SIP_PASSWORD=your-sip-password JIGASI_SIP_SERVER=sip.provider.example JIGASI_SIP_PORT=5060 docker compose -f docker-compose.yml -f jibri.yml -f etherpad.yml -f jigasi.yml up -d That last command is the full stack. Save it as a script, because you need the same file list for every pull , up and down . Step 7: Firewall and Docker sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw allow 10000/udp Docker publishes its ports through iptables directly, so UFW rules alone don't protect containers. Read why Docker bypasses UFW before you assume an internal port is closed. Troubleshooting Two people fine, the third breaks everything: with two participants Jitsi uses a direct peer-to-peer call. From three it goes through the video bridge, so UDP 10000 is blocked or JVB_ADVERTISE_IPS is wrong. "You have been disconnected" right after joining with a token: the token is expired, signed with the wrong secret, or iss doesn't match JWT_APP_ID . Decode it at jwt.io and compare. Recording button missing: ENABLE_RECORDING=1 isn't set, or the Jibri container isn't running. Check docker compose logs jibri . Jibri starts then fails: usually snd-aloop isn't loaded, or the server is out of CPU. Frequently asked questions Is self-hosted Jitsi Meet free? Yes. Jitsi Meet is open source and free to self-host. You pay for the servers and bandwidth, plus a SIP account if you use phone dial-in. How many participants can one Jitsi server handle? It depends on CPU, bandwidth and how many people send video. Test with your real meeting sizes, then scale out by adding video bridges rather than buying one huge server. Can I embed Jitsi in my own app? Yes. The Jitsi Meet IFrame API embeds meetings in any website, and with JWT your backend decides who can join each room and who is moderator. Jitsi or LiveKit? Jitsi is a complete meeting app you can run today. LiveKit is a media server and SDKs for building your own video or voice AI product. I compare them in my LiveKit self-hosting guide . Can Jibri live stream to YouTube? Yes. Jibri can stream a meeting to an RTMP endpoint such as YouTube Live instead of recording to a file. Like recording, each stream uses one Jibri. Need a private Jitsi server for your team or app? I install and maintain Jitsi Meet with JWT login from your app, Jibri recording, Etherpad, Jigasi phone dial-in, HTTPS, backups and monitoring. See my Linux system admin services or tell me how many people join your meetings .

Read article →