← Blog

Self-host Chatwoot with Postgres, Redis and Sidekiq

Railyard

April 11, 2026 · 9 min read

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.

Skip the server setup

Pick Chatwoot in Railyard's templates catalog and get the same four services wired up and deployed without touching a terminal.

Free plan, no card, and 14 days of Pro for every new team. One flat price per team.