Self-host Chatwoot with Postgres, Redis and Sidekiq
Follow this on your own server. Start free, no card needed
Chatwoot gives you a shared inbox and live chat widget without a per-agent SaaS bill, but the project itself is four moving parts: a Rails web process, a Sidekiq worker for background jobs, Postgres, and Redis. Get any one of those wrong (a stale FRONTEND_URL, a worker that can’t reach Redis, a certificate that never issues) and you get a login page that spins forever instead of a support inbox. By the end of this guide you’ll have all four services running on your own server, behind HTTPS, with backups and an upgrade path you can actually repeat.
What you’ll need
- A server with at least 4 vCPUs and 8 GB of RAM. Chatwoot’s own self-hosting docs list 2 cores / 4 GB as the bare minimum, but that’s for the web process alone; once you add the Sidekiq worker, Postgres and Redis on the same box, their docs move the recommendation to 4+ cores and 8 GB+. A Hetzner CX33 (4 vCPU, 8 GB RAM, 80 GB NVMe) fits that recommendation and currently costs €8.99/month excl. VAT.
- Ubuntu 22.04 or newer, with root or sudo access.
- A domain name you can point an A record at (this guide uses
chat.example.com). - Optional but recommended: SMTP credentials from a transactional email provider, so Chatwoot can send password resets and agent notifications. Chatwoot ships with no SMTP configured by default.
Step 1: Point DNS at the server and open the firewall
Create an A record for your subdomain pointing at the server’s public IPv4 address. DNS propagation is why the HTTPS step later can fail if you rush it, so do this first and let it settle while you work through the rest of the setup.
On the server itself, open only the ports you need:
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
You should see Status: active with those three rules listed when you run ufw status.
Step 2: Install Docker
Chatwoot’s Docker deployment needs Docker Engine and the Compose plugin. The official install script sets up both:
curl -fsSL https://get.docker.com | sh
usermod -aG docker $(whoami)
Log out and back in so the group change takes effect, then check both pieces are present:
docker --version
docker compose version
You should see a Docker version and a Compose version printed, not a “command not found” error.
Step 3: Generate the secrets you’ll need
Three values need to exist before you write the compose file: a Rails secret key, a Postgres password, and a Redis password. Generate all three now so you’re not tempted to leave a default in place:
openssl rand -hex 64 # SECRET_KEY_BASE
openssl rand -hex 24 # POSTGRES_PASSWORD
openssl rand -hex 24 # REDIS_PASSWORD
Save the three outputs somewhere you can paste from. Put them in a .env file next to the compose file rather than hardcoding them, so the compose file itself stays safe to keep in version control:
mkdir -p /opt/chatwoot && cd /opt/chatwoot
cat > .env <<'EOF'
SECRET_KEY_BASE=paste-the-first-value-here
POSTGRES_PASSWORD=paste-the-second-value-here
REDIS_PASSWORD=paste-the-third-value-here
SMTP_ADDRESS=smtp.your-provider.com
SMTP_USERNAME=your-smtp-username
SMTP_PASSWORD=your-smtp-password
EOF
chmod 600 .env
Step 4: Write the Compose file
This is the part most guides gloss over: Chatwoot needs the exact same environment variables on both the web container and the Sidekiq container, because Sidekiq runs the same Rails app, just with a different process. Miss one on the worker and background jobs (mailers, webhooks, CSAT surveys) silently fail even though the web UI looks fine.
services:
postgres:
image: pgvector/pgvector:0.8.7-pg16
restart: always
environment:
POSTGRES_USER: chatwoot
POSTGRES_DB: chatwoot
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7.4.11-alpine
restart: always
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
volumes:
- redis-data:/data
rails:
image: chatwoot/chatwoot:v4.18.0
restart: always
depends_on:
- postgres
- redis
ports:
- "127.0.0.1:3000:3000"
environment:
RAILS_ENV: production
NODE_ENV: production
INSTALLATION_ENV: docker
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
FRONTEND_URL: https://chat.example.com
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
POSTGRES_HOST: postgres
POSTGRES_USERNAME: chatwoot
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DATABASE: chatwoot
SMTP_ADDRESS: ${SMTP_ADDRESS}
SMTP_PORT: "587"
SMTP_USERNAME: ${SMTP_USERNAME}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_AUTHENTICATION: plain
SMTP_DOMAIN: example.com
MAILER_SENDER_EMAIL: support@example.com
command: >
bash -c "bundle exec rails db:chatwoot_prepare && bundle exec rails s -p 3000 -b 0.0.0.0"
volumes:
- storage-data:/app/storage
sidekiq:
image: chatwoot/chatwoot:v4.18.0
restart: always
depends_on:
- postgres
- redis
environment:
RAILS_ENV: production
NODE_ENV: production
INSTALLATION_ENV: docker
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
FRONTEND_URL: https://chat.example.com
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
POSTGRES_HOST: postgres
POSTGRES_USERNAME: chatwoot
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DATABASE: chatwoot
SMTP_ADDRESS: ${SMTP_ADDRESS}
SMTP_PORT: "587"
SMTP_USERNAME: ${SMTP_USERNAME}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_AUTHENTICATION: plain
SMTP_DOMAIN: example.com
MAILER_SENDER_EMAIL: support@example.com
command: bundle exec sidekiq -C config/sidekiq.yml
volumes:
- storage-data:/app/storage
volumes:
postgres-data:
redis-data:
storage-data:
Notice the Rails container only binds to 127.0.0.1:3000, not the public interface. Caddy, which you’ll set up next, is what actually faces the internet; the app container should never be directly reachable.
Replace chat.example.com and support@example.com with your real domain before moving on. Check the file parses cleanly:
docker compose config
You should see the full resolved configuration printed back with no error, and no leftover ${...} placeholders in the output.
Step 5: Bring the stack up
docker compose up -d
The rails container’s command runs db:chatwoot_prepare before starting the web server, which creates the database, loads the schema, and seeds the initial data on first boot. Watch it happen:
docker compose logs -f rails
You should see migration output followed by Listening on http://0.0.0.0:3000. Then confirm all four containers are actually up:
docker compose ps
You should see postgres, redis, rails and sidekiq all in a running or healthy state, not restarting.
Step 6: Put Caddy in front with automatic HTTPS
Install Caddy and give it a single site block. Caddy requests and renews the certificate itself the first time it starts, as long as DNS is already pointing at the server:
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install -y caddy
Replace /etc/caddy/Caddyfile with:
chat.example.com {
reverse_proxy localhost:3000
}
Then reload it:
systemctl reload caddy
Visit https://chat.example.com in a browser. You should see a valid padlock and Chatwoot’s setup screen, not a certificate warning.
Step 7: Back up Postgres on a schedule
Everything that matters (conversations, contacts, agent accounts) lives in the postgres volume. A daily dump that you move off the server is the minimum viable backup:
cat > /opt/chatwoot/backup.sh <<'EOF'
#!/bin/sh
cd /opt/chatwoot
docker compose exec -T postgres pg_dump -U chatwoot chatwoot | gzip > /opt/chatwoot/backups/chatwoot-$(date +%F).sql.gz
find /opt/chatwoot/backups -mtime +14 -delete
EOF
chmod +x /opt/chatwoot/backup.sh
mkdir -p /opt/chatwoot/backups
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/chatwoot/backup.sh") | crontab -
Copy at least one of those .sql.gz files off the server (to object storage, another machine, anywhere that isn’t this VPS) so a lost disk doesn’t take your backups with it. Run ls /opt/chatwoot/backups the next morning; you should see a new dated file.
Step 8: Upgrading to a new Chatwoot version
Every Chatwoot upgrade needs the migration command re-run, the same way the first boot did. Pull the new tag, run the migration once, then restart both containers that use the image:
docker compose pull rails sidekiq
docker compose run --rm rails bundle exec rails db:chatwoot_prepare
docker compose up -d rails sidekiq
Check docker compose logs -f rails again. You should see the new version string in the boot log and no migration errors.
The shortcut: do it with Railyard
Everything above (the Postgres instance, the Redis instance with a password, the web process, the Sidekiq worker reading the exact same environment, the storage volume shared between them, HTTPS on your domain) is also available as a template in Railyard’s own templates catalog. Pick Chatwoot there and the four services come up already wired to each other: Postgres and Redis are connected to both the web process and the Sidekiq worker automatically, so there’s no copy-pasting environment variables between two containers and hoping you didn’t miss one. You still add your own SMTP provider afterward, since Railyard doesn’t configure one for you either. If you’d rather start from your own fork or a modified version of the stack, deploying from a repository and the deploy button both work the same way. Backups for the Postgres service are covered in databases and backups, and if this is your first time on Railyard, the quickstart covers account setup before any of this. For a sense of what a box like this costs across providers, see hosting costs.
Troubleshooting
“Port is already allocated” on docker compose up. Something else on the server is already using port 3000, or you ran docker compose up twice without stopping the first run. Check with ss -tlnp | grep 3000 and stop whatever’s holding it, or change the left-hand side of the rails service’s port mapping.
Login page loads but redirects or links are wrong. FRONTEND_URL has to be the exact HTTPS URL agents and customers will use, set on both the rails and sidekiq containers. A mismatch (http instead of https, a trailing slash, the wrong subdomain) breaks password reset links, webhook callbacks and the widget’s own asset URLs even though the app itself boots fine.
Certificate never issues, Caddy logs show repeated “challenge failed”. This is almost always DNS, not Caddy. The A record either hasn’t propagated yet or doesn’t point at this server’s IP. Run dig chat.example.com +short from your own machine and confirm it returns the server’s actual public IP before reloading Caddy again.
File uploads or avatars fail with permission errors. The chatwoot-storage volume needs to be writable by the user the Rails process runs as inside the container. If you changed the volume mount or are migrating from a bind mount, check ownership with docker compose exec rails ls -la /app/storage rather than guessing.
Sidekiq container keeps restarting or jobs never process. Check docker compose logs sidekiq first. A worker getting OOM-killed shows up as the container restarting with no clear error in the log, in which case the fix is more memory for the box, not a config change. If it’s up but jobs just never run, confirm REDIS_URL on the sidekiq service matches the rails service exactly, character for character, including the password.
No emails are sent, ever. Chatwoot ships with no SMTP configured. If you skipped the SMTP variables in Step 4, password resets, agent invitations and conversation notifications all silently do nothing. Add real SMTP credentials and restart the rails and sidekiq containers.
FAQ
Can I run Chatwoot without Sidekiq? No. Background jobs (outgoing email, webhooks, CSAT, automation rules) depend on it; without it the app runs but large parts of it quietly stop working.
Does Chatwoot need Redis for anything besides Sidekiq? Yes, it’s also used for caching and ActionCable (the real-time updates in the agent dashboard), so it’s required even if you don’t think of yourself as using background jobs directly.
Can Postgres and Redis run on a separate, smaller server from the web and worker? Yes, and Chatwoot’s own docs mention this as an option when you want to use managed database services instead of containers. For a single small team, keeping all four on one box is simpler to operate and is what this guide covers.
What’s the difference between the baseline 2 core / 4 GB spec and the 4 core / 8 GB one? The smaller number is for the web process by itself. The larger one is Chatwoot’s own recommendation once Postgres, Redis and the Sidekiq worker are all competing for CPU and memory on the same machine, which is the normal case for a self-hosted install.
Do I have to use the exact image tags in this guide? No, newer tags are released regularly. This guide pins chatwoot/chatwoot:v4.18.0 because that’s the version actually verified end to end; check the project’s release notes before moving to a newer tag, and re-run the migration step either way.
A working Chatwoot install is four small, well-understood pieces talking to each other correctly, not one complicated thing. Once DNS, the environment variables and the migration step are right, there’s very little left to maintain beyond backups and the occasional version bump.
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.