Running Parletto in production

What comes after the quick start on your own server: TLS, email, file uploads, backups, upgrades and troubleshooting.

Start with Self-hosting Parletto, which covers the image, the storage volume, the environment variables and a first run. This page covers what comes after that on a real server: TLS, email, file uploads, backups, upgrades and troubleshooting. It follows the deployment guide in the Parletto source, which is opening shortly under the MIT license.

The commands below use the container name parletto, as in the docker run steps in the source’s README.

Server requirements

  • Minimum: 1 GB RAM, 1 CPU core, 10 GB disk.
  • Recommended: 2 GB RAM, 2 CPU cores, 20 GB disk.
  • Any Linux host that runs Docker. Ports 80 and 443 must be reachable if Thruster handles TLS.

Redis, PostgreSQL and a separate web server are not needed. SQLite holds everything: app data, cache (Solid Cache), the job queue (Solid Queue) and WebSocket pub/sub (Solid Cable).

How the container runs

  • On every start, bin/docker-entrypoint runs bin/rails db:prepare. On an empty volume this creates the four databases (production, production_cache, production_queue, production_cable). On later starts it applies any pending migrations.
  • A fresh production database has no accounts. Sign up to create the first one.
  • The server is Puma behind Thruster. Thruster listens on port 80, and on port 443 too when TLS_DOMAIN is set.
  • Background jobs run inside Puma when SOLID_QUEUE_IN_PUMA=true. To run them separately, start a second container from the same image and volume with the command bin/jobs.
  • /up is the health check. It returns 200 when the app has booted.
  • Secrets (SMTP, R2, the asset cookie) come from config/credentials/production.yml.enc, decrypted with RAILS_MASTER_KEY. config/credentials.example.yml in the source lists every key. Without credentials, the app boots with only SECRET_KEY_BASE set: email then does not work, and uploads are stored on local disk.

TLS

Production sets config.force_ssl and config.assume_ssl, so cookies are Secure and HSTS is sent. Pick one of these:

  • Thruster: set TLS_DOMAIN=chat.example.com and publish ports 80 and 443. Certificates come from Let’s Encrypt and are stored in /rails/storage/thruster, inside the volume.
  • Your own reverse proxy (Caddy, nginx, Traefik, a load balancer): leave TLS_DOMAIN unset, terminate TLS in the proxy and forward to the container’s port 80. The proxy must send X-Forwarded-Proto: https and pass WebSocket upgrades for /cable.

If you use Cloudflare in front of the server, set the SSL/TLS mode to “Full”.

DISABLE_SSL=true turns off force_ssl and assume_ssl. The Compose quick start sets it so you can try Parletto on http://localhost:3000. Remove it before you expose the server, unless a proxy in front of it deliberately serves plain HTTP.

Docker Compose on a server

  1. In .env, delete DISABLE_SSL=true and set TLS_DOMAIN and ACTION_MAILER_HOST to your domain.
  2. In docker-compose.yml, publish "80:80" and "443:443" instead of 3000:80, or keep the port and put your own TLS proxy in front.
  3. For SMTP and R2, create production credentials (the README in the source walks through it), uncomment the production.yml.enc mount, and set RAILS_MASTER_KEY in .env.

Email (SMTP)

Email is used for organization invitations and password resets. The app runs without it, but those emails will fail to send.

SMTP settings are read from the production credentials. Add an smtp section to the production.yml you write when creating the credentials, and encrypt it again:

smtp:
  user_name: apikey                # SendGrid uses the literal string "apikey"
  password: YOUR_SMTP_PASSWORD
  address: smtp.sendgrid.net       # any SMTP server works
  port: 587
  authentication: plain
  from_email: team@your-domain.com

Then restart the container with the new production.yml.enc, and set ACTION_MAILER_HOST to your domain so that links in emails point to it.

File uploads: local disk or Cloudflare R2

Parletto uses the r2 storage service when the credentials contain r2.bucket, and local disk (/rails/storage, inside the volume) otherwise.

Local disk (default). Nothing to configure. Avatars, organization icons and message attachments are saved in the volume and served by the app at /uploads/<key>. Before sending a file, the app checks that the signed-in user is a member of the file’s organization (for avatars: the user themselves or someone sharing an organization). Everyone else gets a 404. Back up the volume to keep uploads.

Cloudflare R2 and the asset Worker (optional). Use this to keep uploads off the server and serve them from a CDN. It needs an R2 bucket and credentials in the r2 section, an asset_cookie section with a cdn_host, and the Worker in workers/asset-auth deployed with Wrangler. The app does not hand out direct R2 URLs: it builds https://<cdn_host>/<key> URLs, and the Worker serves them after checking a signed cookie the app issues. Your app and assets hosts must share a parent domain so the cookie reaches both. The deployment guide and docs/r2-setup.md in the source cover the steps.

Backups

All data is in the storage volume. To make a consistent copy of the main database while the app is running, use SQLite’s backup command:

docker exec parletto sqlite3 /rails/storage/production.sqlite3 ".backup '/rails/storage/backup-$(date +%Y%m%d).sqlite3'"
docker cp parletto:/rails/storage/backup-$(date +%Y%m%d).sqlite3 ./backups/

The cache, queue and cable databases can be recreated, so of the databases only production.sqlite3 needs a backup. Uploads stored on local disk are files in the same volume (/rails/storage, excluding the *.sqlite3 files), so copy those too. If you store uploads in R2, back them up separately. Keep backups off the server.

Upgrades

Take a backup first. Migrations run automatically when the container starts.

With Docker Compose

docker compose pull && docker compose up -d

If you build from source, git pull and run docker compose up -d --build instead.

With docker run

git pull origin main
docker build -t parletto .
docker stop parletto && docker rm parletto
docker run -d --name parletto ...   # same flags, volume and credentials as before

Troubleshooting

Start with docker logs parletto.

Container exits on boot
Missing secret_key_base or ActiveSupport::MessageEncryptor::InvalidMessage: with Compose and no credentials, SECRET_KEY_BASE is missing from .env. With credentials, RAILS_MASTER_KEY does not match the mounted production.yml.enc, or the file is not mounted.
Signed out immediately or sign-in does not stick
Cookies are Secure, so you need HTTPS everywhere except localhost, unless DISABLE_SSL=true. Check that TLS is set up and that your proxy sends X-Forwarded-Proto: https.
Real-time updates not arriving
Solid Cable stores messages in production_cable.sqlite3, so check that the volume is mounted and writable. A reverse proxy in front of the app must pass WebSocket upgrades for /cable.
Emails not sending
Check the smtp credentials and ACTION_MAILER_HOST, and that background jobs are running (SOLID_QUEUE_IN_PUMA=true or a bin/jobs container). To test from a console:
docker exec -it parletto bin/rails runner \
  'ActionMailer::Base.mail(from: "test@example.com", to: "you@example.com", subject: "Test", body: "Test").deliver_now'
Uploads fail or images do not show
For a 404 from /uploads/..., check that you are signed in as a member of the file’s organization. For a 401 or 403 from the assets host, check that the Worker’s COOKIE_SECRET matches asset_cookie.secret, that the R2 bindings are right, and that the parletto_assets cookie is set on your parent domain.
Database locked errors
Only one container should write to a given volume, apart from an optional bin/jobs container. Check free disk space and bin/rails db:migrate:status.

Security checklist

  1. Never commit config/credentials/*.key or .env files.
  2. Serve the app over HTTPS only.
  3. Keep production.key (RAILS_MASTER_KEY) private and backed up, and keep it out of the repo directory when you build images.
  4. Back up production.sqlite3 regularly and store the backups off the server.
  5. Rebuild regularly to pick up Ruby, gem and npm security updates.