The Simplest Possible Deployment: SQLite, Gunicorn, One Server

BudgetFlow runs on one server with Flask, Gunicorn, and SQLite. The setup is small enough that I can see where a request goes and which process handles it.

Gunicorn listens on localhost, and nginx forwards requests to it. The production SQLite file lives at /var/lib/budgetflow/db.sqlite3. The app configures its database connection in db/__init__.py.

These are the settings I use, along with the limits I watch for.

The SQLite Configuration

The connection handler sets five SQLite options whenever SQLAlchemy opens a new connection:

@event.listens_for(engine, "connect")
def do_connect(conn, _conn_record):
	conn.isolation_level = None
	conn.execute("PRAGMA journal_mode = WAL;")
	conn.execute("PRAGMA busy_timeout = 5000;")
	conn.execute("PRAGMA synchronous = NORMAL;")
	conn.execute("PRAGMA cache_size = -256000;")
	conn.execute("PRAGMA foreign_keys = ON;")

Each setting has a specific job.

journal_mode = WAL lets readers continue while a writer has an active transaction. SQLite still allows only one writer at a time.

busy_timeout = 5000 gives a connection up to five seconds to wait for a lock. A timeout is still possible if a transaction holds the lock too long.

synchronous = NORMAL reduces sync calls in WAL mode. It improves write performance, with a durability tradeoff if the operating system or machine fails before data reaches durable storage.

cache_size = -256000 sets a target page cache of about 250 MiB per connection. The negative value is measured in kibibytes, so memory use matters if the app opens several connections.

foreign_keys = ON enables enforcement of foreign key constraints for the connection.

The engine also starts transactions this way:

@event.listens_for(engine, "begin")
def do_begin(conn):
	conn.execute(text("BEGIN IMMEDIATE"))

BEGIN IMMEDIATE reserves the write lock when the transaction begins. This avoids a read transaction needing to upgrade its lock later, but it also means a read-only transaction can make another writer wait. Short transactions matter here.

Why One Worker

The Gunicorn config is short:

workers = 1
keepalive = 5
timeout = 30
bind = "127.0.0.1:8000"

One synchronous worker processes one request at a time. That suits BudgetFlow's current traffic and avoids concurrent requests in the Gunicorn process. It does not mean SQLite requires one worker; SQLite can serve multiple processes, subject to its single-writer limit.

If requests queue or response times grow, the worker count is one of the first settings to revisit. The effect depends on traffic, query duration, and time spent waiting for external services.

Binding to 127.0.0.1:8000 keeps Gunicorn off the public network. nginx handles incoming traffic.

What Sits in Front

nginx sits in front of Gunicorn as a reverse proxy. The request path looks like this:

client -> nginx -> Gunicorn (127.0.0.1:8000) -> Flask -> SQLite

This leaves Gunicorn listening only on localhost. nginx can handle TLS and static files while forwarding application requests to Flask.

Backups

The database is a file, so copies of that file need to be consistent and stored away from the server. The repo's justfile includes a command to download the production database and its WAL files for local use.

For online backups, SQLite provides a backup API that can create a consistent snapshot while the app is running. Whatever method you use, a restore test is the way to find out whether those copies are usable.

When to Outgrow This

This setup has limits. I would revisit it when one of these becomes measurable:

Those are reasons to change the deployment when they arise. Until then, this setup is easier for me to operate and inspect.

Why This Setup Works Here

BudgetFlow's current traffic fits on one server. Fewer services means fewer configuration files and fewer places to investigate when a request fails.

The constraints are visible too: one synchronous worker and one SQLite writer. I can monitor response times and lock waits, then change the setup if either becomes a problem.

For this application, the simplest useful deployment is the one I can maintain and restore reliably.

Start Your Financial Journey Today

Take control of your financial future with visual planning and dynamic budgeting. Sign up now and start managing your budget with confidence!