Django SmallStack uses SQLite as its default database—and that's a deliberate choice, not a compromise. For solo developers, small teams, and internal applications, SQLite is often the best database option, not just the easiest one.
See also: PostgreSQL Database for when you need to scale beyond SQLite.
SQLite has experienced a renaissance in the web development community. What was once dismissed as "just for development" is now recognized as a production-ready database for many use cases.
"SQLite is likely used more than all other database engines combined. SQLite is in every iPhone and every Android phone, every Mac and every Windows 10 machine, every Firefox, Chrome, and Safari browser." — sqlite.org
The SQLite developers acknowledge it's likely the most widely deployed database engine in existence, largely due to its ubiquity as an embedded database in mobile devices, set-top boxes, automotive systems, and consumer electronics.
| Use Case | Why SQLite Works |
|---|---|
| Internal tools | Single-user or small team, infrequent writes |
| Admin dashboards | Read-heavy workloads are SQLite's strength |
| Prototypes & MVPs | Ship fast, migrate later if needed |
| Personal projects | Zero configuration, zero cost |
| Content sites | Blogs, documentation, portfolios |
| Budget-conscious deployments | No database service fees |
The project separates the database file from the application code, which is critical for containerized deployments.
/app/
├── apps/ # Application code (disposable)
├── static/ # Static files (disposable)
├── data/ # Database lives here (PERSISTENT)
│ └── db.sqlite3
└── media/ # Uploads (PERSISTENT)
In config/settings/development.py:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"OPTIONS": SQLITE_OPTIONS, # WAL, IMMEDIATE, performance PRAGMAs
}
}
In config/settings/production.py:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": config("DATABASE_PATH", default="/app/data/db.sqlite3"),
"OPTIONS": SQLITE_OPTIONS,
}
}
SQLITE_OPTIONS is defined in base.py and includes WAL mode, IMMEDIATE transactions, and performance PRAGMAs. See the Best Practices section below for details.
Containers are ephemeral—they can be destroyed and recreated at any time. By mounting the data/ directory as a volume, your database persists across container rebuilds:
# docker-compose.yml
volumes:
- ./data:/app/data # Local development
# deploy.yml (Kamal)
volumes:
- /root/app_data/db:/app/data # VPS deployment
Key insight: The database lives on your VPS filesystem, not inside the container. When you deploy a new version, the container is replaced but the data remains untouched.
Because SQLite is just a file, backups are remarkably simple. Django SmallStack includes a built-in backup system with scheduled backups, a staff dashboard, and one-click downloads — no configuration required beyond enabling the cron schedule.
# Create a backup from the command line
make backup
# Or download your database from the staff dashboard at /backups/
The backup tool uses Python's sqlite3.Connection.backup() API to create safe, non-blocking copies while your application continues serving requests. Combined with VPS-level snapshots and periodic downloads to your local machine, you get solid multi-layer data protection.
Read the full Database Backups guide for setup instructions, scheduling, and backup strategy recommendations.
For manual or ad-hoc backups outside the built-in tool:
VPS snapshots — Most providers offer automated snapshots. Enable them and your database is automatically backed up.
File copy — With WAL mode enabled, the database uses three files: db.sqlite3, db.sqlite3-wal, and db.sqlite3-shm. A plain cp of the main file alone may produce a corrupt backup. Use sqlite3 .backup or make backup instead:
```bash
# UNSAFE with WAL mode:
# cp /root/app_data/db/db.sqlite3 /root/backups/db-$(date +%Y%m%d).sqlite3
# SAFE — use the built-in backup command instead: make backup ```
bash
sqlite3 /root/app_data/db/db.sqlite3 ".backup '/root/backups/db-backup.sqlite3'"Django SmallStack applies a set of PRAGMAs automatically via SQLITE_OPTIONS in base.py:
SQLITE_OPTIONS = {
"transaction_mode": "IMMEDIATE",
"timeout": 5,
"init_command": (
"PRAGMA journal_mode=WAL;"
"PRAGMA synchronous=NORMAL;"
"PRAGMA temp_store=MEMORY;"
"PRAGMA mmap_size=134217728;"
"PRAGMA journal_size_limit=27103364;"
"PRAGMA cache_size=2000;"
),
}
| Setting | What it does |
|---|---|
journal_mode=WAL |
Allows concurrent reads while a write is in progress |
synchronous=NORMAL |
Safe with WAL; reduces fsync overhead |
transaction_mode=IMMEDIATE |
Acquires write lock upfront, preventing "database is locked" errors from lock upgrades |
timeout=5 |
Waits up to 5 seconds for the write lock instead of failing immediately |
temp_store=MEMORY |
Keeps temporary tables in memory |
mmap_size=128MB |
Memory-maps the database file for faster reads |
journal_size_limit |
Prevents the WAL file from growing unbounded |
cache_size=2000 |
2000 pages (~8 MB) of page cache |
SQLite allows one writer at a time, but with WAL mode enabled, reads are never blocked by writes. Combined with IMMEDIATE transactions, concurrent writers queue properly instead of failing with lock errors. This is fine for:
Consider PostgreSQL if you need: - ❌ Sustained high concurrent write volume beyond single-writer capacity - ❌ Multiple servers writing to the same database - ❌ Advanced features (full-text search, JSON operators, PostGIS)
Consider hosting a small Django application:
With Managed PostgreSQL:
| Item | Monthly Cost |
|---|---|
| VPS (basic) | $5-10 |
| Managed PostgreSQL | $15+ |
| Total | $20-25+ |
With SQLite:
| Item | Monthly Cost |
|---|---|
| VPS (basic) | $5-10 |
| Database | $0 (included) |
| Total | $5-10 |
Savings: $10-15/month ($120-180/year) per application
For a solo developer running multiple small projects, SQLite can save hundreds of dollars annually.
The "SQLite in production" movement has gained significant momentum:
The web development community is recognizing that not every application needs PostgreSQL. For many projects, SQLite's simplicity, reliability, and zero-cost operation make it the pragmatic choice.
You'll know it's time to consider PostgreSQL when:
Until then, SQLite serves admirably. And when you do need to migrate, the path is well-documented.
Read the PostgreSQL Migration Guide
| Task | Command |
|---|---|
| Access SQLite shell | sqlite3 data/db.sqlite3 |
| Backup database | sqlite3 data/db.sqlite3 ".backup 'backup.sqlite3'" |
| Check database size | ls -lh data/db.sqlite3 |
| Vacuum (reclaim space) | sqlite3 data/db.sqlite3 "VACUUM;" |
| Check integrity | sqlite3 data/db.sqlite3 "PRAGMA integrity_check;" |
# Access SQLite shell
kamal app exec "sqlite3 /app/data/db.sqlite3"
# Run Django shell
kamal app exec "python manage.py dbshell"
Choose the color mode for your app.
The accent color for your app.
Choose the font family that fits your app.
Choose the gray shade for your app.
Choose the border radius factor for your app.
Choose the page layout for your app.