Self-host Rocket.Chat for team chat
Follow this on your own server. Start free, no card needed
Slack’s per-seat pricing scales with headcount, not with how much your team actually chats, and the moment you need every message to stay on infrastructure you control, a hosted SaaS inbox stops being an option at all. Rocket.Chat is an open-source team chat platform that does the same job, channels, direct messages, file sharing, search, and you run it yourself. The one thing that makes it harder to self-host than most chat tools is that it needs MongoDB configured as a replica set, not a plain standalone database, because Rocket.Chat depends on MongoDB’s change streams to push messages to connected clients in real time. By the end of this guide you’ll have a working Rocket.Chat instance on your own Hetzner server, behind HTTPS, with that replica set correctly initialized, backups, and a safe way to upgrade later.
What you’ll need
- A Hetzner Cloud account and a server. Rocket.Chat plus a MongoDB replica set needs more headroom than the smallest instances, so this guide uses the CX33 (4 vCPU, 8 GB memory, 80 GB NVMe), listed at €8.99/month excluding VAT. Check current pricing in the Hetzner Cloud Console before ordering, since lineups and prices do shift.
- A domain name (or a subdomain of one you already own), with access to its DNS settings.
- An SSH key pair on your machine (
ssh-keygen -t ed25519if you don’t already have one). - Docker and the Docker Compose plugin installed on the server (covered in Step 2).
- A terminal and about twenty minutes, mostly spent waiting on DNS and the initial sync.
Step 1: Create the server and point a domain at it
In the Hetzner Cloud Console, open or create a project, then choose Add Server. Pick a location: Nuremberg, Falkenstein or Helsinki keep your data in the EU, while Ashburn, Hillsboro or Singapore are also available if your team is elsewhere. Choose Ubuntu 24.04 as the image, then under server type pick the Shared vCPU CX33 (4 vCPU, 8 GB memory, 80 GB NVMe). Under SSH keys, add the public key from your machine instead of relying on a generated password, then create the server.
Once it boots, note its public IPv4 address and connect to confirm access:
ssh root@YOUR_SERVER_IP
You should land at a root shell with the Ubuntu welcome banner, no password prompt.
In your domain’s DNS settings, add an A record pointing a subdomain, for example chat.yourdomain.com, at that IP:
Type: A
Name: chat
Value: <your server's IP>
TTL: 300
DNS propagation can take a few minutes to a few hours. Confirm it from your own machine before moving on, since Caddy’s certificate request in Step 5 depends on it:
dig chat.yourdomain.com +short
You should see your server’s IP printed back.
Step 2: Install Docker
SSH into the server and install Docker Engine plus the Compose plugin:
apt-get update
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
systemctl enable --now docker
Hetzner doesn’t put a firewall in front of a new server by default, so lock the host down with ufw:
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable
Check the install:
docker --version
docker compose version
You should see a Docker version and a Compose version printed, not a “command not found” error.
Step 3: Lay out the project and its secrets
Create a directory for the stack and an .env file to hold the domain and anything else that shouldn’t live in the compose file itself:
mkdir -p /opt/rocketchat && cd /opt/rocketchat
cat > .env <<'EOF'
DOMAIN=chat.yourdomain.com
EOF
chmod 600 .env
Rocket.Chat doesn’t take an admin username or password as container configuration the way some apps do. The first admin account is created later, through the app’s own setup wizard, so there’s no credential to generate or store at this stage beyond the domain itself.
Step 4: Write the Docker Compose file
This is the part where the replica-set requirement actually shows up. The mongo service needs to start mongod with --replSet rs0, then check whether a replica set is already initialized, and if not, initiate one with itself as the sole member. That’s a compound shell command, not a single binary invocation, so it has to run through a shell rather than as mongod’s direct arguments.
Create docker-compose.yml:
services:
rocket-chat:
image: rocketchat/rocket.chat:8.5.4
restart: unless-stopped
environment:
- PORT=3000
- ROOT_URL=https://${DOMAIN}
- DEPLOY_METHOD=docker
- MONGO_URL=mongodb://mongo:27017/rocketchat?replicaSet=rs0
- MONGO_OPLOG_URL=mongodb://mongo:27017/local?replicaSet=rs0
volumes:
- rocketchat_uploads:/app/uploads
depends_on:
- mongo
networks:
- rocketchat-net
mongo:
image: mongodb/mongodb-community-server:8.0-ubi8
restart: unless-stopped
command: ["bash", "-c", "mongod --replSet rs0 --bind_ip_all --fork --logpath /data/db/mongod.log && mongosh --quiet --eval \"try { rs.status().ok } catch (e) { rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'mongo:27017'}]}) }\" && tail -f /data/db/mongod.log"]
volumes:
- mongo_data:/data/db
networks:
- rocketchat-net
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
- DOMAIN=${DOMAIN}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- rocket-chat
networks:
- rocketchat-net
networks:
rocketchat-net:
volumes:
rocketchat_uploads:
mongo_data:
caddy_data:
caddy_config:
A few things worth explaining:
mongo’scommanddoes three things in sequence: startmongodas a background (--fork) process bound to all interfaces with replication enabled, then runmongoshto checkrs.status().ok, catching the error that call throws on a fresh database and callingrs.initiate()with this container as the only member ofrs0if that happens, and finallytail -fthe log file so the container has a foreground process to stay alive on. On every restart after the first,rs.status().okalready succeeds, sors.initiate()is skipped and the container just starts serving.- Neither
rocket-chatnormongogets asecurity_opt: ["no-new-privileges:true"]entry, and that’s deliberate, not an oversight. MongoDB’s own entrypoint drops root privileges by re-executing itself as a subprocess, and that re-exec needs the ability to gain new privileges. Adding the usual hardening flag here breaksmongod --forkwith a generic “child process failed, exited with 1” and no clearer error in the log. - Both images are pinned to exact tags,
rocket.chat:8.5.4andmongodb-community-server:8.0-ubi8, rather thanlatest. A chat server that silently changes database behavior on a restart is not something you want to discover during a workday. - Only Caddy publishes ports to the host. Rocket.Chat and MongoDB are reachable only on the internal Docker network.
Check the file parses cleanly:
docker compose config
You should see the fully resolved configuration printed with no error lines.
Step 5: Write the Caddyfile and start everything
Create Caddyfile in the same directory:
{$DOMAIN} {
reverse_proxy rocket-chat:3000
}
Caddy requests a free TLS certificate from Let’s Encrypt for your domain automatically, as long as DNS already resolves to this server and ports 80 and 443 are reachable from the internet, which Step 1 and Step 2 already set up.
Start the stack:
docker compose up -d
Watch the logs while Mongo comes up:
docker compose logs -f mongo
The first startup takes longer than later restarts, since it has to initialize the replica set before Rocket.Chat can connect. Once that settles, check Caddy got its certificate:
docker compose logs -f caddy
You should see a line mentioning a certificate obtained for your domain. Confirm everything is up:
docker compose ps
All three services should show as Up.
Step 6: Create the first admin account
Visit https://chat.yourdomain.com in a browser. Rocket.Chat’s own setup wizard loads, asking for an organization name, then an admin account, email and password. This is Rocket.Chat’s first-run flow, not something the compose file configures for you, so there’s nothing to script here beyond getting through it once. After that, the wizard offers a few optional steps, registration settings, a federation prompt, which you can skip or configure as you like; none of it blocks the instance from working.
Step 7: Back up the database and the uploads volume
Two things need backing up: the MongoDB data (channels, messages, users, settings) and the rocketchat_uploads volume (files shared in chat). A mongodump against the running container, piped straight to a compressed archive:
docker compose exec -T mongo mongodump --archive --gzip > rocketchat-backup-$(date +%F).gz
Put that in a daily cron job, and copy the result somewhere other than this server, an object storage bucket, another machine, anywhere that isn’t this box. A backup that lives only on the server it’s protecting against doesn’t survive the failure mode that actually takes a server down.
For the uploads volume:
docker run --rm -v rocketchat_rocketchat_uploads:/data -v $(pwd):/backup alpine \
tar czf /backup/rocketchat-uploads-backup.tar.gz -C /data .
Keep both files somewhere access-controlled; a message history backup is still real user data.
Step 8: Upgrade deliberately
Both images are pinned, so an upgrade is a conscious choice, not something that happens on a routine restart. Before bumping either tag, read Rocket.Chat’s own release notes for every version between what you’re running and what you’re moving to. Rocket.Chat, like many apps with a long-running history, can require stepping through intermediate major versions in sequence rather than jumping straight from an old release to the newest one, so check that before assuming a single jump is safe.
Bump the tag in docker-compose.yml:
image: rocketchat/rocket.chat:8.6.0
Then:
docker compose pull
docker compose up -d
Check the logs for a clean startup with no migration error, and confirm your channels and message history are intact after logging back in. If a migration genuinely goes wrong, your backup from Step 7 is the way back to the previous version; restore it against the older image tag before deciding what to do next.
The shortcut: do it with Railyard
Everything above, a dedicated MongoDB replica set correctly initialized, the uploads volume, HTTPS already wired up, is also available as a one-click deploy, but because it’s a multi-service stack rather than a single container, the link doesn’t deploy it directly the way a one-container app template would:
https://app.railyard.run/deploy?stack=rocket-chat
That link opens the templates catalog instead of deploying straight away, since a stack needs you to confirm its services before it provisions anything. Pick Rocket.Chat in the templates catalog from there. Railyard wires up the same replica-set requirement this guide walks through by hand, the same persistent uploads volume, and HTTPS on your domain, without you writing the compose file or the shell command inside the Mongo container yourself.
Deploy button explains how the ?stack= link and the templates catalog flow work if you want to understand or embed one elsewhere. Databases and backups covers how backups work across templates generally, not just this one. If you’re starting from Railyard before deciding on Rocket.Chat specifically, the quickstart covers the basics. Railyard’s Free plan covers one server plus a database server and three apps with no card required, Pro is $19/month per team, and Team is $49/month per team.
Troubleshooting
The replica set never initializes, or rs.status().ok keeps failing. Check docker compose logs mongo for the actual error from the rs.initiate() call. The most common cause is the hostname in members not matching what the rocket-chat service actually resolves; in this compose file that host is the service name mongo, so if you’ve renamed the service, the host: value inside the command needs to match it exactly.
Mongo fails to start with mongod --fork: "child process failed, exited with 1". This is almost always security_opt: ["no-new-privileges:true"] added to the mongo service, usually from applying a general container-hardening pass across every service in a compose file without an exception for this one. MongoDB’s entrypoint re-executes itself as a subprocess to drop root privileges, and that re-exec needs the ability to gain new privileges; the hardening flag blocks exactly that. Remove it from the mongo service specifically.
Rocket.Chat loads but every link, avatar or redirect points at the wrong address. This is a ROOT_URL mismatch. It needs to be the exact HTTPS URL you actually visit the instance at, including the scheme; if it’s set to http:// instead of https://, or to a different hostname than the one Caddy is serving, you’ll see redirect loops in the browser or broken asset URLs in the page source.
Caddy never gets a certificate, or the browser shows a certificate warning. This is almost always DNS, not Caddy. Run dig chat.yourdomain.com +short and confirm it returns this server’s IP, not an old one or nothing at all. Let’s Encrypt also needs port 80 reachable from the internet for the initial validation step, so check your firewall allows inbound 80, not only 443.
Mongo gets killed or the server becomes unresponsive under normal use. A replica set, even a single-node one, keeps more in memory than a plain standalone Mongo instance, and Rocket.Chat itself is not a light process. If you’re on a smaller server than the CX33 this guide uses, that’s the first thing to check with docker stats before assuming something else is wrong; moving up a size is usually the actual fix, not a tuning flag.
FAQ
Can I skip the replica set and run Mongo as a plain standalone instance? No. Rocket.Chat uses MongoDB’s change streams, which only exist on a replica set, to push new messages and presence updates to connected clients without polling. A standalone Mongo will let Rocket.Chat start, but real-time updates won’t work correctly, so this isn’t an optional hardening step you can skip for a smaller footprint.
Is this the classic Rocket.Chat or the microservices version? This is the classic monolith, one rocket-chat container talking to one database, which is the right fit for most team sizes and the simplest thing to operate yourself. Rocket.Chat also publishes a microservices split for very large deployments; its own documentation covers that path if you ever need to scale past what a single container handles.
How much does this cost to run? The server is €8.99/month excluding VAT for the CX33 this guide uses. Rocket.Chat itself is free, open-source software with no per-seat charge; the cost is the box it runs on, not a license.
What happens if a migration fails during an upgrade? Rocket.Chat’s startup runs its own internal migrations against the database on boot. If one fails, the container’s logs will show it clearly rather than the app silently coming up half-migrated. Your options at that point are to restore the backup from Step 7 against the previous image tag, or to read the specific migration error and the corresponding release notes before trying again; don’t retry blindly against the same database state.
Do I need a separate email service to invite users? Not to get the instance running; the admin account from Step 6 can create and invite users directly from the admin panel without outgoing email configured. You’ll want SMTP configured eventually for password resets and notification emails, but it’s not a blocker for a working first deploy.
A self-hosted Rocket.Chat instance is a modest amount of setup for something you’ll likely leave running for years: your team’s message history stays on a server you control, with a backup routine and an upgrade path that don’t depend on anyone else’s release schedule.
Why people pick Railyard for this
- Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
- One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
- One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
- Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
- Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.