Skip to content
All articles
7 min read

Docker "Permission Denied" on docker.sock: Safe Fix

MD Rakibul Islam RakibMD Rakibul Islam RakibFull-stack developer, DevOps & Linux engineer
Docker "Permission Denied" on docker.sock: Safe Fix

Docker "permission denied" on docker.sock means your user isn't allowed to use the Docker socket. Join the docker group and log back in, or use rootless Docker.

The full error looks like this: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock. You see it the first time you run docker ps without sudo on a new server, and again when a CI runner, Jenkins or a deploy user tries to build images. The internet's favourite fix, sudo chmod 666 /var/run/docker.sock, works and is the worst choice: it gives every user and process on the machine root access. I run Docker on servers where a non-root deploy user ships releases, so here are the fixes I actually use and what each one costs you in security.

Key takeaways

  • The socket is owned by root:docker with mode 660. Only root and members of the docker group can talk to the daemon.
  • The standard fix is sudo usermod -aG docker $USER, then log out and back in. Group changes don't apply to sessions that are already open.
  • The docker group is root-equivalent. Docker's own docs say so: anyone in it can mount the host filesystem into a container.
  • Rootless Docker runs the daemon as your user, so a container escape doesn't hand out root. It's the safer choice on shared machines.
  • Never chmod 666 the socket. It hands root to every local user, and it resets on reboot anyway.

Step 1: Make sure it's a permission problem

Two errors look similar but have different fixes:

  • "permission denied while trying to connect to the Docker daemon socket": the daemon is running, your user just isn't allowed to use it. Keep reading.
  • "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?": the daemon is stopped or crashed. Check sudo systemctl status docker and sudo journalctl -u docker -n 50.

Then look at the socket and at your groups:

ls -l /var/run/docker.sock
# srw-rw---- 1 root docker 0 Oct  7 09:12 /var/run/docker.sock
id
# uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo)

If docker isn't in the list from id, that's the whole problem.

Fix 1: Add your user to the docker group

This is the method in Docker's post-install guide:

sudo groupadd docker          # usually exists already; an error here is fine
sudo usermod -aG docker $USER
newgrp docker                 # applies to this shell only
docker run --rm hello-world

The -a matters: without it, usermod -G replaces all your other groups, including sudo. If it still says permission denied, your session predates the change. Log out and back in; for SSH, close the connection and reconnect. Three places keep the old groups longer than people expect:

  • tmux and screen sessions started before the change. Start a new session.
  • VS Code Remote-SSH keeps a server process running. Use "Kill VS Code Server on Host" and reconnect.
  • Services such as a GitHub Actions self-hosted runner or Jenkins only pick up new groups after a restart: sudo usermod -aG docker jenkins && sudo systemctl restart jenkins.
youdocker ps + docker .sockroot:dockerrw-rw---- dockerdruns as root denied Whoever can open the socket controls a root daemon.
The Docker socket only lets in root and the docker group. Joining the group opens it, but the daemon behind it runs as root, which is why group membership is effectively root access.

Why the docker group is effectively root

The daemon runs as root, and the socket is its full control API. Anyone who can reach it can run this:

docker run --rm -it -v /:/host alpine chroot /host sh   # a root shell on the host

That's not a bug, it's how Docker works, and Docker's docs warn that the group "grants root-level privileges". So treat membership like sudo without a password: give it only to people and service accounts you'd trust with root. On a single-purpose server where your deploy user already has sudo, the docker group adds little risk. On a shared machine or a CI runner that builds untrusted pull requests, prefer rootless mode.

Fix 2: Rootless Docker

Rootless mode runs a separate Docker daemon as your own user, inside a user namespace. A process that breaks out of a container lands as your unprivileged user, not root. On Ubuntu with Docker's official packages:

sudo apt-get install -y uidmap docker-ce-rootless-extras
dockerd-rootless-setuptool.sh install
systemctl --user enable --now docker
sudo loginctl enable-linger $USER          # keep it running after you log out
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
docker run --rm hello-world

Put the DOCKER_HOST line in your ~/.bashrc, or use docker context use rootless. Trade-offs to know before switching: images and containers live in your home directory, separate from the root daemon's; binding ports below 1024 needs extra setup (proxy 80/443 from Nginx on the host instead); and some networking and resource-limit features behave differently. The rootless docs list the current limitations.

Fix 3: Keep using sudo

Typing sudo docker is a perfectly good answer for admins on production servers. Every command is logged in the auth log, nobody gets silent root, and you can limit which users have it. For deploy scripts, I prefer a dedicated deploy user in the docker group over giving it general passwordless sudo.

Special cases

A container that mounts docker.sock

Tools like Portainer, Traefik or a CI container mount /var/run/docker.sock and then get permission denied because the docker group ID inside the container differs from the host's. Pass the host's group ID instead of running the container as root:

docker run --group-add "$(stat -c %g /var/run/docker.sock)" \
  -v /var/run/docker.sock:/var/run/docker.sock my-ci-image

Remember that whatever you hand the socket to also gets root on the host. A read-only mount doesn't change that, since the API still works. Only mount it into containers you trust, or put a socket proxy in front that allows just the API calls the tool needs.

GitHub Actions self-hosted runners

Add the runner's user to the docker group, then restart the runner service so it picks up the group (sudo ./svc.sh stop && sudo ./svc.sh start in the runner directory). If the runner builds pull requests from forks, use rootless Docker or ephemeral runners instead.

It worked, then broke after a reboot

That's the chmod 666 fix wearing off: Docker recreates the socket with root:docker 660 every time it starts. Use the group instead.

For containers that start and then keep dying, see my Docker restart loop guide. And if you publish container ports on a server with UFW, read why Docker bypasses UFW before you go live.

Frequently asked questions

Why do I still get permission denied after adding myself to the docker group?

Group membership is read when you log in, so existing sessions don't have it. Log out and back in, reconnect SSH, start a new tmux session, or run newgrp docker in the current shell. Check with id that docker appears.

Is it safe to chmod 666 /var/run/docker.sock?

No. It lets every user and process on the machine control a root daemon, which is the same as giving them root. It also resets when Docker restarts. Use the docker group, rootless mode or sudo.

Is adding a user to the docker group a security risk?

Yes, it's equivalent to giving that user root, because they can mount the host filesystem into a container. That's acceptable for trusted admins and deploy users on a single-purpose server, but not for untrusted users or shared machines.

What's the difference between rootless Docker and the docker group?

With the docker group, your user controls a daemon running as root. With rootless Docker, the daemon itself runs as your user, so a container escape only gets your user's permissions. Rootless is safer but has some limits on networking and low ports.

How do I fix Jenkins "permission denied" on docker.sock?

Add the jenkins user to the docker group with sudo usermod -aG docker jenkins and restart Jenkins so it gets the new group. If Jenkins runs in a container, pass the socket's group ID with --group-add.

Want Docker set up safely?

I set up Docker and CI/CD on Linux servers with least-privilege deploy users, firewalls that actually apply to containers, and automated deploys with rollback. See my DevOps services, or contact me with what you're running and the error you see.

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.

  • docker permission denied
  • docker.sock permission denied
  • docker group
  • rootless Docker
  • Cannot connect to the Docker daemon
  • Ubuntu
  • DevOps

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 →