
SQLite is a good fit for a lot of small and mid-sized workloads. Install it with sudo apt install sqlite3, which takes a few seconds on Ubuntu 26.04 LTS. Getting a recent version, the right dev libraries, and sane production settings takes a bit more care. The sections below cover each of those.
Why SQLite Still Deserves a Place on Your Server
A typical story: a developer spins up a small internal tool, a WordPress-adjacent microservice, or a crawler that stores URLs and status codes. Someone suggests PostgreSQL. Someone else suggests MySQL. Then the whole thing turns out to be one process, one writer, a few hundred megabytes of data, and a requirement that backups be trivial. Standing up a database server for that is overkill. Another daemon to patch, another port to firewall, another set of credentials to rotate, and another thing that can fail at 3 a.m.
SQLite removes that whole layer. It is a library, not a server. The database is one file, and your application talks to it directly. There is no network socket to expose and no service to restart. That is why it sits inside browsers, phones, routers, and countless Linux tools you already use.
Ubuntu 26.04 LTS, codenamed Resolute Raccoon, was released on 23 April 2026 and gets standard security support until April 2031. The 26.04.1 point release has since landed, so it is now a comfortable base for production. The release is built on Linux 7.0, and it ships Rust-based utilities such as sudo-rs and uutils coreutils. That matters for one practical reason: some of your muscle-memory commands may behave very slightly differently, so test your scripts instead of assuming.
This guide covers three things:
- Installing SQLite with APT, the right choice for most people.
- Building a newer version from source when the packaged one is not enough.
- Configuring, securing, backing up, and troubleshooting SQLite so it holds up under real traffic.
Along the way, the reasoning behind each step is spelled out, because copy-pasting commands you do not understand is how outages start.
What You Actually Install: CLI, Library, and Headers
Many “SQLite is not working” tickets come from people mixing up three different pieces.
- The
sqlite3command-line shell. This is the interactive tool for creating databases, running queries, and exporting data. - The runtime library (
libsqlite3-0). Python, PHP, Perl, and many other programs link against this. It is usually installed already because dozens of packages depend on it. - The development package (
libsqlite3-dev). This provides headers and the static and shared library symlinks. You need it only when compiling software that links to SQLite.
So installing the shell does not give you headers, and having the library does not give you the shell. Know which one your task requires before you start.
Prerequisites Before You Begin
You need very little:
- An Ubuntu 26.04 LTS system (server, desktop, container, or WSL).
- A user with
sudoprivileges. - A working internet connection or a configured local mirror.
- Around 10 MB of disk space for the packaged version, more if you compile from source.
Confirm your release first. It takes two seconds and saves confusion later:
lsb_release -a
cat /etc/os-release
You should see Ubuntu 26.04 and the codename resolute. If you are still on 24.04, the official route to 26.04 runs through the point-release upgrade. Do not force it on a production box. Test on a clone first.
Method 1: Install SQLite With APT (Recommended)
This is the approach to use unless you have a specific reason not to. Packages from the Ubuntu archive receive security patches, integrate with unattended-upgrades, and are what every other package on the system expects.
Step 1: Refresh the package index
sudo apt update
This pulls current package metadata. Skipping it is a classic cause of “404 Not Found” errors when APT tries to fetch a package version that has already been superseded.
Step 2: Install the SQLite command-line tool
sudo apt install sqlite3
APT resolves the dependency on libsqlite3-0 automatically and installs the sqlite3 binary into /usr/bin.
Step 3: Verify the installation
sqlite3 --version
The output shows the version, a build timestamp, and a source hash. At the time of writing, the Ubuntu package page for sqlite3 in the resolute suite lists version 3.46.1-9ubuntu0.1. Package versions can change as security updates arrive, so trust your own apt output over any blog post, this one included. To see what your mirror offers:
apt policy sqlite3
apt changelog sqlite3 | head -40
The changelog is underrated. It tells you which CVEs were backported, which is how Ubuntu keeps an older upstream number safe without bumping the version.
Step 4: Install the development headers (only if needed)
sudo apt install libsqlite3-dev
You need this when you build Python extensions, Ruby gems like sqlite3, Node modules such as better-sqlite3 against the system library, or any C project. If a build fails with fatal error: sqlite3.h: No such file or directory, this is the missing piece.
Step 5: Optional companions
sudo apt install sqlite3-tools sqlitebrowser
sqlitebrowser is a GUI (DB Browser for SQLite) and makes sense on a desktop, not a headless server. The sqlite3-tools package may not exist as a separate package on every release, so check with apt search sqlite3 if APT complains. Whether it is available depends on your archive, and there is no point guessing.
For language bindings:
sudo apt install php-sqlite3 # PHP (PDO SQLite and SQLite3 extensions)
sudo apt install python3 # sqlite3 module is built in
Python’s standard sqlite3 module ships with the interpreter and links to the system library. You can see which engine version Python is actually using:
python3 -c "import sqlite3; print(sqlite3.sqlite_version)"
That one-liner has resolved more version-mismatch arguments than any documentation page.
Your First Database: A Quick Sanity Test
Installing is half the job. Prove it works end to end.
mkdir -p ~/sqlite-lab && cd ~/sqlite-lab
sqlite3 crawl.db
Inside the shell:
CREATE TABLE urls (
id INTEGER PRIMARY KEY,
url TEXT NOT NULL UNIQUE,
status INTEGER,
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO urls (url, status) VALUES
('https://example.com/', 200),
('https://example.com/old-page', 404);
SELECT * FROM urls;
.headers on
.mode column
SELECT status, COUNT(*) AS total FROM urls GROUP BY status;
.quit
A file named crawl.db now exists in the directory. That is the entire database. Copy it, email it, or back it up with rsync. For anyone coming from MySQL or PostgreSQL, that simplicity takes a little time to sink in.
A handful of dot-commands are worth memorizing:
| Command | What it does |
|---|---|
.tables |
Lists tables |
.schema urls |
Shows the CREATE statement |
.headers on |
Prints column names |
.mode column or .mode box |
Readable output |
.import file.csv table |
Loads CSV data |
.dump |
Exports SQL text |
.backup file |
Online, consistent backup |
.timer on |
Shows query time |
Method 2: Build the Latest SQLite From Source
The packaged version is stable and patched, but it is often a few releases behind. SQLite’s own site lists 3.53.4 as the latest release, dated 24 July 2026. You may want the newer build for a feature, a performance fix, or a bug fix. One notable item: the 3.53.4 release notes mention a fix for a WAL-reset database corruption bug. If you run heavy concurrent WAL workloads, that is worth reading in full before deciding whether to stay on the distro package. Check whether Ubuntu has backported the fix by searching the package changelog for the relevant CVE or bug reference.
The rule of thumb in production: stay with the distro package unless you can name the specific reason you need a newer one. Compiling puts you in charge of patching.
Step 1: Install build dependencies
sudo apt update
sudo apt install build-essential wget tar libreadline-dev zlib1g-dev
libreadline-dev gives the shell arrow-key history and line editing. Without it, you get a shell that feels like a 1995 terminal.
Step 2: Download the amalgamation source
SQLite publishes an “autoconf” tarball. The version number is encoded as 3 followed by two digits each for minor and patch, with trailing 00. So 3.53.4 becomes 3530400:
cd /usr/local/src
sudo wget https://www.sqlite.org/2026/sqlite-autoconf-3530400.tar.gz
Open sqlite.org/download.html to confirm the current filename and year path. Then verify the checksum shown on that page:
sha3sum -a 256 sqlite-autoconf-3530400.tar.gz
If sha3sum is not installed, sudo apt install libdigest-sha3-perl provides it. Checksum verification takes a minute and protects you from corrupted or tampered downloads.
Step 3: Extract and configure
sudo tar xzf sqlite-autoconf-3530400.tar.gz
cd sqlite-autoconf-3530400
sudo ./configure --prefix=/opt/sqlite-3.53.4
Installing into /opt/sqlite-3.53.4 rather than /usr/local or /usr is deliberate. It keeps the custom build separate from the packaged one, so apt never fights you and rollback means deleting a directory. Overwriting files in /usr/lib by hand is the quickest way to break a package manager.
Step 4: Compile with useful options
Compile-time flags change SQLite’s capabilities. A sensible set for server work:
export CFLAGS="-O2 \
-DSQLITE_ENABLE_FTS5 \
-DSQLITE_ENABLE_JSON1 \
-DSQLITE_ENABLE_RTREE \
-DSQLITE_ENABLE_COLUMN_METADATA \
-DSQLITE_ENABLE_DBSTAT_VTAB \
-DSQLITE_SECURE_DELETE \
-DSQLITE_THREADSAFE=1"
sudo -E ./configure --prefix=/opt/sqlite-3.53.4
make -j"$(nproc)"
Why these? FTS5 gives full-text search. R-Tree supports spatial indexing. DBSTAT_VTAB lets you inspect page usage. SQLITE_SECURE_DELETE overwrites deleted content, which is useful when the data is sensitive, at a slight write cost. Recent releases already enable several of these by default, so duplicates are harmless.
Step 5: Install and test
sudo make install
/opt/sqlite-3.53.4/bin/sqlite3 --version
Step 6: Make it usable
Add it to your path for one user:
echo 'export PATH=/opt/sqlite-3.53.4/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
which sqlite3
To point a program at the newer library at runtime:
LD_LIBRARY_PATH=/opt/sqlite-3.53.4/lib python3 -c "import sqlite3; print(sqlite3.sqlite_version)"
Notice this only changes what that single command loads. Python’s sqlite3 module still links dynamically to whichever libsqlite3.so the loader finds first, which is exactly why the environment variable trick works. For a service, set it in the systemd unit instead:
[Service]
Environment=LD_LIBRARY_PATH=/opt/sqlite-3.53.4/lib
Avoid setting LD_LIBRARY_PATH system-wide. A global override can change behavior for unrelated software, and those bugs are miserable to track down.
Installing SQLite on Debian, AlmaLinux, and Other Distributions
Most readers manage a mixed fleet, so here is the equivalent on common distributions.
# Debian
sudo apt update && sudo apt install sqlite3 libsqlite3-dev
# AlmaLinux / Rocky / RHEL
sudo dnf install sqlite sqlite-devel
# Fedora
sudo dnf install sqlite sqlite-devel
# Arch
sudo pacman -S sqlite
# Alpine (containers)
apk add sqlite sqlite-dev
Package names differ. On RPM-based systems the CLI lives in sqlite, not sqlite3, although the binary is still called sqlite3. That inconsistency trips up people writing cross-distro Ansible roles.
Configuring SQLite for Real Workloads
A default SQLite database is conservative. It favors safety and compatibility over throughput. For web workloads, a few PRAGMA settings make a very large difference.
Enable WAL mode
PRAGMA journal_mode = WAL;
Write-Ahead Logging lets readers keep working while a writer commits. In the default rollback-journal mode, writers block readers. For a site with traffic spikes, that difference is dramatic. WAL mode is persistent: it is stored in the database file, so you set it once.
One caveat. WAL requires shared memory and does not work reliably on network filesystems like NFS. Keep the database on local disk.
Set a sensible synchronous level
PRAGMA synchronous = NORMAL;
With WAL, NORMAL is considered safe against application crashes and, in most cases, power loss only risks the most recent transactions rather than corruption. FULL is stricter and slower. Choose based on how painful losing the last second of writes would be.
Handle contention gracefully
PRAGMA busy_timeout = 5000;
Without it, a second writer gets “database is locked” immediately. Five seconds of waiting resolves nearly all short collisions. This is a per-connection setting, so your application must issue it on every new connection.
Other settings worth knowing
PRAGMA foreign_keys = ON; -- off by default, per connection
PRAGMA cache_size = -64000; -- about 64 MB page cache (negative = KiB)
PRAGMA temp_store = MEMORY; -- temp tables in RAM
PRAGMA mmap_size = 268435456; -- 256 MB memory-mapped I/O
foreign_keys surprises people. Foreign key constraints exist in the schema but are ignored unless you enable them on each connection. That is not a bug, just a historical default.
Performance Tuning: CPU, RAM, Disk I/O, and Network
SQLite performance is mostly about disk behavior and transaction discipline. The CPU is rarely the bottleneck.
Disk I/O
The single most common performance mistake is running many small INSERT statements without a surrounding transaction. Each autocommit insert forces a sync to disk. Wrapping 10,000 inserts in one BEGIN; ... COMMIT; can turn minutes into a fraction of a second. If you ever see a script crawling at a few hundred rows per second, check this first.
Put the database on SSD or NVMe storage. Check device behavior with:
iostat -xz 2
sudo iotop -oPa
High await and %util during writes signals the disk is saturated. Also keep the -wal and -shm files on the same filesystem as the database file.
RAM
Use cache_size and mmap_size to let the kernel and SQLite keep hot pages in memory. Monitor with free -h and vmstat 2. Memory-mapped I/O helps read-heavy workloads on 64-bit systems, but over-allocating on a small VPS pushes the box into swap, which defeats the purpose.
CPU
Queries that scan entire tables burn CPU. Use the planner:
EXPLAIN QUERY PLAN
SELECT * FROM urls WHERE status = 404;
If it reports SCAN urls, add an index:
CREATE INDEX idx_urls_status ON urls(status);
ANALYZE;
PRAGMA optimize;
Run PRAGMA optimize periodically, or just before closing long-lived connections, so the planner has current statistics.
Network
SQLite has no network layer, which is a feature. The catch: never place a live database on NFS or SMB. File locking over network mounts is unreliable and corruption follows. If multiple hosts need access, you have outgrown SQLite for that use case, or you should look at replication tools such as Litestream or LiteFS, which stream changes from a local database.
Handy diagnostics
sqlite3 crawl.db "PRAGMA integrity_check;"
sqlite3 crawl.db "PRAGMA page_count; PRAGMA page_size; PRAGMA freelist_count;"
sqlite3 crawl.db "VACUUM;"
VACUUM rebuilds the file and reclaims space, but it needs free disk roughly equal to the database size. On a nearly full volume, it fails or worsens the situation.
Security Considerations
Because there is no server process, the attack surface looks small. It is smaller, not zero.
File permissions
The database file is guarded by ordinary Unix permissions. Lock it down:
sudo mkdir -p /var/lib/myapp
sudo chown myapp:myapp /var/lib/myapp
sudo chmod 750 /var/lib/myapp
sudo chmod 640 /var/lib/myapp/app.db
SQLite creates -wal and -shm files next to the database, so permissions apply to the directory as well. If the application user cannot write to the directory, you get read-only errors even when the file itself is writable.
Keep it out of the web root
This one shows up in audits all the time. A .db file placed under /var/www/html can be downloaded by anyone who guesses the filename. Store databases outside the document root. As a backstop, block these extensions in Nginx:
location ~* \.(db|sqlite|sqlite3|db-wal|db-shm)$ {
deny all;
return 404;
}
Reload with sudo nginx -t && sudo systemctl reload nginx.
Prevent SQL injection
Use parameterized queries in every language. In Python:
cur.execute("SELECT * FROM urls WHERE status = ?", (status,))
String concatenation into SQL is still the number one way SQLite-backed apps get compromised.
Patch regularly
Install security updates automatically:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Ubuntu’s main repository gets CVE patching for the support window mentioned earlier. If you compiled from source, you own that burden. Subscribe to SQLite release announcements and rebuild when patch releases appear.
Firewall
SQLite itself opens no ports, so there is nothing to firewall for the database. The application in front of it still needs rules:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Sandboxing with systemd
For services using SQLite, hardening options reduce blast radius:
[Service]
User=myapp
ProtectSystem=strict
ReadWritePaths=/var/lib/myapp
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=strict makes the whole filesystem read-only for the service except the paths you list. Forget ReadWritePaths and SQLite will report that it cannot open or write the database, which is a confusing symptom if you have forgotten you added the hardening.
Backups That Actually Work
Copying a live database with cp is risky. If a write happens mid-copy, the result may be inconsistent, and in WAL mode you might miss data sitting in the -wal file. Use SQLite’s own tools.
sqlite3 /var/lib/myapp/app.db ".backup '/backup/app-$(date +%F).db'"
Or the SQL form:
VACUUM INTO '/backup/app-snapshot.db';
VACUUM INTO produces a compacted, consistent copy. Recent SQLite versions also include sqlite3_rsync, which synchronizes a live database to a replica efficiently. Check whether your installed build includes it.
A basic cron job:
# /etc/cron.d/myapp-backup
15 2 * * * myapp sqlite3 /var/lib/myapp/app.db ".backup '/backup/app-$(date +\%F).db'" && find /backup -name 'app-*.db' -mtime +14 -delete
Note the escaped \% in cron. An unescaped percent sign is treated as a newline and silently breaks the job. Then test a restore. A backup you have never restored is only a hope.
Troubleshooting Common Errors
sqlite3: command not found
The CLI is not installed, or your path is wrong.
sudo apt install sqlite3
hash -r
which sqlite3
Error: database is locked
Another connection is holding a write lock. Causes include a long-running transaction, a forgotten open shell, or no busy_timeout.
sudo lsof /var/lib/myapp/app.db
sudo fuser -v /var/lib/myapp/app.db
Enable WAL, set busy_timeout, and shorten transactions. Killing processes is the last resort.
attempt to write a readonly database
Almost always permissions on the directory, not the file. Also check for ProtectSystem in systemd, read-only mounts, and SELinux or AppArmor denials:
ls -ld /var/lib/myapp
mount | grep myapp
sudo journalctl -xe | grep -i denied
fatal error: sqlite3.h: No such file or directory
Install the headers: sudo apt install libsqlite3-dev.
unable to open database file
Wrong path, missing parent directory, or the process cannot traverse the directory. Use absolute paths in services, because the working directory under systemd is not your home.
Version mismatch between CLI and application
The shell reports 3.53.4 but Python reports an older number. They link to different libraries. Compare:
sqlite3 --version
python3 -c "import sqlite3; print(sqlite3.sqlite_version)"
ldd $(which sqlite3) | grep sqlite
database disk image is malformed
This indicates corruption. Stop writes, copy the files, and try recovery:
sqlite3 broken.db ".recover" | sqlite3 recovered.db
Then investigate the cause: network filesystems, a full disk, killed processes during unsafe settings, or failing storage. Check dmesg and smartctl -a /dev/nvme0n1.
E: Unable to locate package sqlite3
Enable the Universe repository if it was removed, then update:
sudo add-apt-repository universe
sudo apt update