Rails SQLite Deployment: Single Server or Postgres?
A practical Rails SQLite deployment decision guide: when one server, Solid Queue and Solid Cache fit, and when managed PostgreSQL is safer in production.
A Rails system I run in production has a 100 GB PostgreSQL database, more than 4 TB of media, nearly 7 million observations, and over 17 million attachments, which is why SQLite on one server would be the wrong call, not slightly suboptimal, but wrong.
Single-server Rails is easy to discuss as though it were universal - SQLite, Solid Queue, Solid Cache, a persistent disk, Kamal, and few moving parts - and for the right app that is a very good default, although the decision is whether your system remains one computer's problem or has become a data platform, integration system, or operations dependency that needs a different shape.
I have not deployed a production Rails system on SQLite, Hetzner, and Kamal; the live Rails platform I can point to uses PostgreSQL, Hatchbox, DigitalOcean, DigitalOcean Spaces, Solid Queue, and Solid Cache, and it runs a UK property inventory agency's daily operations, client portal, imports, webhooks, report archive, and invoice access, so the single-server argument is engineering judgement, not a story about a deployment I did not make.
One server is a design choice, not a cheaper version of architecture
A single server puts the app, database, queue workers, cache, and scheduled jobs on one machine, so requests do not cross the public internet to reach Redis, background jobs do not need a managed broker, and a person who built the app can inspect processes and disk space, leaving fewer failure modes, while Solid Queue stores jobs in the application database and Solid Cache stores cache entries there, so with SQLite a small product can have one database file and persistent storage instead of PostgreSQL, Redis, job and cache services, and their credentials, bills, alerts, and backup policies.
The saving is the number of things that can be half-configured, because every extra service needs backups, access control, upgrades, connection limits, monitoring, an owner, and a 2am decision; Kamal adds a repeatable container deployment over SSH, health checks, and a clear release process without making the team platform operators, while Hetzner is one host for this shape and operational rules around the box matter more, which works when the business has one clear failure domain: a small internal tool can accept maintenance, while a new SaaS with a handful of customers may be better served by shipping and restoring a backup than by paying for untested high availability, although you should review that choice as the product changes.
SQLite works well when writes are calm and the data is replaceable
SQLite is a mature, fast transactional database with a small operational surface, and I would consider it when five conditions hold: first, one writer at a time is acceptable, because although SQLite permits many readers, users must mostly read, edit their records occasionally, and create few background jobs; a content site with accounts, small back-office tool, personal product with bursty writes, or narrowly scoped service with a few paying teams can fit.
Second, data loss is bounded and recoverable: if the database is corrupt at 10:00, can you restore last night's backup and replay changes from an upstream system, payment provider, or support record; if not, reconsider the backup frequency and recovery procedure before considering SQLite.
Third, the application does not need independent scaling, so one web and one job process can share a machine, which can gain CPU, memory, and disk, but if jobs need a different CPU profile from web traffic, or the web app needs a second machine for availability, the database is the boundary you cannot ignore; fourth, storage is boring, because the database must live on durable local storage or a correctly mounted persistent volume, never an ephemeral container filesystem, and locally stored Active Storage files need the same care because deploys must preserve the database, uploads, ownership, and permissions; fifth, the team can stop writes briefly for maintenance, because a long migration can block SQLite's single writer, so keep migrations small, schedule the ones that are not, and understand the lock before deploying, although few screens do not make an app small if it has busy imports, many webhook deliveries, or a growing document pile.
Solid Queue removes Redis, but it does not remove contention
Solid Queue keeps job state with the data it changes, so instead of a request writing to PostgreSQL, handing work to Redis, and a worker reading PostgreSQL again, a job can be enqueued with the record creation, inspected with the same tools, and backed up with the same database - a clean early-product trade, not a guarantee that every job workload fits SQLite - while an app that receives an order, emails a receipt, makes a PDF, and runs a nightly report can use SQLite plus Solid Queue, with short, retryable jobs, modest request volume, and one worker, perhaps two for independent work, although the worker count must still respect the database's write behaviour.
An integration service is different because it receives vendor webhooks, pulls paginated records, writes source snapshots, projects canonical tables, creates documents, and retries errors, while its queue coordinates the business and every job competes to write state with users, so choose PostgreSQL before that system is visibly slow; the property platform is in this category: it is a Rails monolith using Solid Queue and Solid Cache, with PostgreSQL holding business records, integration state, audit entries, source snapshots, and Solid Queue metadata, while a paginated importer stores source responses before projection, resumes from persisted cursors, and avoids replacing newer webhook values with older imports, alongside signed webhooks, dead-letter controls, replay operations, OAuth connections, and long-running historical work.
A webhook that keeps daily operations current cannot sit behind a large media download, so heavyweight media work stays away from webhook-critical queues - that is queue topology in one Rails app, and where one queue on one database file becomes an operational constraint; Solid Cache is also database activity, so do not let expensive cache writes compete with a payment, webhook, or access-control change.
The crossover happens at operational coupling, not a row count
There is no honest universal threshold: ten thousand rows can be too much when a concurrent ingestion process updates every row, while ten million can be fine as rarely queried append-only history, so use four questions instead: important writes can arrive together - a user save and webhook update, an import result and dependent enqueue, or a payment callback and support account change - and frequent collisions turn SQLite's one-writer model into a business constraint.
Does the queue have a recovery objective, because if a failed process can wait until morning for someone to press retry, simplicity is often right, but if incoming events must be recorded promptly because the next action depends on them, use a base with headroom for several workers and distinct workloads; one copy must restore the complete system, although a SQLite file is easy to copy while the app is stopped and gets harder when it points to files, reports, uploads, and external callbacks, because a backup must restore the database and related object storage to a compatible point in time; adding a second server ends the local database model, because two web machines cannot use a live SQLite database, so do not substitute shared storage, but use a client-server database or keep the app on one box.
The property platform crosses every line: it has over 25,000 properties and more than 60,000 inspections, nearly 7 million observations tied to inspection history, over 17 million attachments pointing to more than 4 TB of media, a 100 GB database, scheduled work, live webhooks, client-facing documents, and third-party API limits that force restartable imports; it is an archive with inspections dating to 2011, where losing a historical report or attaching a photo to the wrong property could remain undiscovered for years, while an earlier image-download path omitted cover images and meter photographs without UUIDs, but a later check confirmed all 17,595 distinct cover-image URLs and all 42,494 meter-photo URLs in the verification set were preserved, while source deletion waited for an unrelated, incomplete actions backfill, which is why I want PostgreSQL, object storage, and infrastructure that can separate concerns, because a cheaper database would not make the important work cheaper.
Managed PostgreSQL earns its cost before you need a cluster
PostgreSQL does not require a large architecture: one Rails monolith, app server, worker, managed PostgreSQL database, and object storage is still quiet, with a database built for remote connections, concurrent writers, independent backups, and future process separation, while the live property platform pairs PostgreSQL with JSONB-backed integration and snapshot data, Active Storage uses DigitalOcean Spaces for current media and a read-only legacy Spaces service for historical media adoption, and Hatchbox and DigitalOcean are the documented production path, so managed services move some responsibility to a provider, but not the questions only an application owner can answer.
A managed PostgreSQL backup is not proof you can restore the application, so run restore drills and verify the restored database can find matching media and that credentials, workers, and migrations behave after rollback, while the platform stores source-system snapshots next to canonical records, integration and audit state, and cursors that advance only after successful imports, retries 429 and temporary 5xx responses with a bounded policy, and has dead-letter and replay controls; PostgreSQL does not supply those things, but it is the right foundation for that simultaneous state and recovery work, so choose managed PostgreSQL when you expect regular inbound webhooks, restartable imports, meaningful audit history, background work that cannot wait behind a deploy, multiple people editing shared records, or a credible need for more than one server, because you can still keep Solid Queue, Solid Cache, one Rails app, and one small app server while PostgreSQL removes a limit without demanding a distributed system.
A small deployment still needs a recovery plan
Provisioning without recovery advice is dangerous, because a short deploy command does not answer what happens when the disk fills, the host disappears, or a migration goes wrong, so before calling SQLite ready, answer: where is the database file, which process writes it, where do uploads live, how are both copied off-machine and how often, can a backup restore to a new server without improvised permissions, how long is acceptable outage, who hears when the worker stops, and how does a schema-changing release roll back?
A volume snapshot can preserve a bad migration, deleted directory, or compromised machine, so keep independent, encrypted backups outside the server with a retention period and restore one on a schedule, because the restore test matters more than the backup provider's status page; with local Active Storage, copy timing matters, because copying the database at 01:00 and files at 01:30 can leave database references without files, so briefly stop writes, use a filesystem-aware backup, store files in object storage, or document an accepted recovery-point gap, but do not pretend the gap does not exist; likewise, a health check proves a container answers one route, not that its worker can read the database, a migration completed, or uploads are mounted, so put those checks in the release process, keep database-changing releases small, and say before running a migration if it cannot be reversed.
I would rather see a small product on one understood server than five managed services nobody has restored, and an operational platform on PostgreSQL, object storage, and tested recovery rather than a single server kept alive by optimism.
Hosting is a trust problem goes deeper on the operational side of this decision. For a Rails build where data volume and integrations may change the deployment choice, my CV and rates are here.