Guide
Why does SQLite say "database is locked" when a container's data is on an NFS or SMB share?
SQLite coordinates writers with file locks and, in WAL mode, with shared memory. A network share breaks one or both. We ran SQLite against SMB and NFS mounts to see which error each failure produces, and what the popular mount-option fixes actually do.
Why does SQLite say database is locked when a Docker container's config is on an NFS or SMB share?
Because SQLite relies on the filesystem's byte-range locks, and network shares implement them differently. On an SMB mount with default options, SQLite could not take its write lock at all and reported database is locked on the first write, with one container. nobrl or NFS nolock make the error go away by keeping locks on one client, so a second writer elsewhere loses data. WAL mode needs shared memory on one host. Keep the config directory on local disk and put only media on the share.
Because SQLite coordinates every write with byte-range locks on the database file, and a network share either refuses those locks or keeps them in a place another machine cannot see. SQLite's FAQ says it plainly: "fcntl() file locking is broken on many NFS implementations", and you "should avoid putting SQLite database files on NFS if multiple processes might try to access the file at the same time". On SMB it is worse. On a CIFS mount with default options, SQLite could not take its write lock at all, and a single container got database is locked on its very first write.
The fix is a layout, not a mount option: the app's config or data directory, where the .db file lives, goes on a disk local to the machine running the container. Media, documents and downloads can stay on the share.
Two answers circulate, and the popular fix removes the lock, not the error#
The first answer in most forum threads is a mount option: nobrl for SMB, nolock for NFS. It works, in the sense that the error stops. It works by telling the client kernel to keep locks to itself and never send them to the server. One container on one host then behaves, because its locks are honoured locally. Anything that opens the same file through another mount, a second container on another host, an app running on the NAS itself, a sync or backup job, gets no exclusion at all. That is the corruption case SQLite's own documentation describes: a filesystem whose locking does not "behave as advertised", with two processes writing at once.
Part of why the mount-option answer persists is the Servarr wiki. It does say "If you're using an NFS mount ensure nolock is enabled" and "If you're using an SMB mount ensure nobrl is enabled", but under the question of why Sonarr cannot see files on a remote server, which is the media mount. A few paragraphs earlier, the same wiki says the *Arr AppData folder, the /config mount for Docker, "MUST be on local storage", and that SQLite on network drives "will cause a malformed database eventually".
The second answer, "never put SQLite on a share", is the right one.
What we measured#
We exported one directory from a kernel NFS server and from Samba 4.15.13, both over loopback, and mounted it several ways on Linux kernel 5.4. Tests ran with SQLite 3.31.1 on the host and SQLite 3.53.4 inside a python:3.13-alpine container, with a bind mount of each share and with a Docker cifs volume. "Two clients" means two separate mounts of the same export.
| Mount | One writer | Two writers, overlapping | WAL, two clients |
|---|---|---|---|
| Local ext4 | works | second gets database is locked, correctly | n/a |
| NFSv4.2, NFSv3 with lockd | works, WAL switches on | second gets database is locked, correctly | disk I/O error on the client that had not written last, and its next write failed |
NFSv3 nolock | works, WAL switches on | both proceed; first commit fails with disk I/O error, its row missing | first commit fails with locking protocol |
| SMB 3.1.1, default | database is locked on the first write, after the 5 s busy timeout we set | never got that far | never got that far |
SMB 3.1.1 nobrl | works, WAL switches on | both proceed; a commit reported as successful is missing | a committed write silently lost |
The last cell is the one to remember. In WAL mode on two nobrl mounts, both connections reported success for every insert, and afterwards one row was not in the database. We ran it three times and lost the same write each time. PRAGMA integrity_check returned ok in every one of these runs, including the ones with missing rows. A database that loses writes this way passes the check that most people use to decide it is healthy.
The container results matched the host results. A bind mount of the default SMB mount and a Docker volume with type=cifs both gave database is locked after 5.02 s.
Why SMB fails with one container#
The mount.cifs manual page explains the option everyone adds. nobrl means: "Do not send byte range lock requests to the server. This is necessary for certain applications that break with cifs style mandatory byte range locks (and most cifs servers do not yet support requesting advisory byte range locks)."
SQLite is one of those applications. Its corruption guide says the default locking mechanism on unix is POSIX advisory locking, and on a plain SMB mount those requests become cifs-style mandatory locks on the server. In our test the first CREATE TABLE on a brand new file waited out its whole busy timeout and failed, with no other process involved. The same shape shows up on the Jellyfin forum: a Docker install with its config on a CIFS share, //192.168.50.30/docker/jellyfin/config, failing logins with SQLite Error 5: 'database is locked', and a thread that went looking for corruption from an unclean shutdown instead.
So on SMB, database is locked is not a sign of contention. Adding retries or a longer timeout does nothing, because the lock is never granted.
Why NFS works until a second client appears#
NFS is subtler, which is why it fools people for weeks. With locking working, through the NFSv4 protocol itself or NFSv3 with the lock manager, two clients excluded each other correctly in our tests. A single container with its database on NFS ran without complaint, WAL included.
The trouble is in what you do not control. SQLite's FAQ warns that its locking "might not work correctly if the database file is kept on an NFS filesystem", and its corruption guide says lock bugs are "especially true of network filesystems and NFS in particular". The server side of that locking lives on the NAS, and a NAS reboot, a lock service that is off, or a client mounted with nolock changes the rules without telling SQLite. Jellyfin's documentation even describes NFSv3 shares with locking unavailable, and lists nolock as one of the remedies for slow playback. That advice is for media files. It is safe there because nothing writes to a video file while it plays.
WAL needs memory that two machines cannot share#
WAL mode adds a second requirement on top of locks. SQLite's WAL documentation lists it as the first disadvantage: "All processes using a database must be on the same host computer; WAL does not work over a network filesystem. This is because WAL requires all processes to share a small amount of memory and processes on separate host machines obviously cannot share memory with each other." That memory is the -shm file next to the database, mapped into every process that opens it.
From one client, that mapping works over NFS and over nobrl SMB, which is why WAL switched on in every single-client test. With two clients, each mapped its own view of the index, and we got disk I/O error on NFS and a silently lost write on SMB. Some apps turn WAL on themselves. Vaultwarden does it at every start, and its wiki says some filesystems, "especially true for network filesystems", make the service "fail to start with Failed to turn on WAL error message". WAL is also persistent: once a database is in WAL mode it reopens in WAL mode. Grafana, which leaves WAL off by default, documents exactly that for its wal setting, and says setting it back to false returns the database to DELETE journal mode on the next start.
What each app's own documentation says#
Most self-hosted apps keep their state in SQLite by default. In this index that includes Jellyfin, Sonarr, Radarr, Prowlarr, Uptime Kuma, Audiobookshelf, Vaultwarden, Grafana and Home Assistant, plus Plex, Navidrome, Kavita, Pi-hole, Gitea, Forgejo and dozens more. Where the projects address network storage, they agree:
| App | Default database | What its docs say about network storage |
|---|---|---|
| Jellyfin | SQLite | "The Jellyfin database should also be stored locally and not on a network storage device." |
| Sonarr, Radarr (Servarr) | SQLite, PostgreSQL optional | /config "MUST be on local storage"; network drives "will cause a malformed database eventually" |
| Uptime Kuma | SQLite | "File Systems like NFS (Network File System) are NOT supported. Please map to a local directory or volume." |
| Audiobookshelf | SQLite | /config must not be "stored and accessed over the network"; it "may appear to work correctly", then "you will eventually encounter database corruption" |
| Vaultwarden | SQLite, WAL on by default | Network filesystems may fail with Failed to turn on WAL; ENABLE_DB_WAL=false turns it off |
| Grafana | SQLite, WAL off by default | Does not address network storage; recommends MySQL or PostgreSQL for production |
| Home Assistant | SQLite (home-assistant_v2.db in /config) | The recorder documentation does not address network storage |
Apps that also support PostgreSQL or MySQL give you a way out when the app genuinely must live on another machine from its data. A database server speaks over the network by design, and the share problem disappears.
The layout that works#
Config on local disk, media on the share. For Jellyfin in Compose:
services:
jellyfin:
image: jellyfin/jellyfin
volumes:
- /srv/jellyfin/config:/config # local SSD on this host
- /srv/jellyfin/cache:/cache # local, regenerable
- /mnt/nas/media:/media:ro # NFS or SMB mount, read only is fineThe same split applies to every app in the table: the folder that holds the .db file stays local, the library goes wherever you like. On a virtual machine whose only large storage is the NAS, give the VM a local virtual disk for config, even a small one. On a ZFS host, Jellyfin's documentation also asks for a 4K or 8K record size on the dataset holding its SQLite database and 1M for media, one more reason to keep the two apart; ZFS, btrfs, mdadm or one disk covers how to split datasets. And if a local folder then refuses writes, that is ownership, not locking, which Why a container gets permission denied on a bind mount covers.
To move an existing config off a share: stop the container, copy the whole folder including any -wal and -shm files, and run sqlite3 app.db 'PRAGMA integrity_check;' on the copy before switching the bind mount. Given what we saw, an ok proves the file is consistent, not that every write made it, so keep the old folder until you have checked recent data in the app.
When this answer stops applying#
- Real second machines. Our "two clients" were two mounts on one kernel. Separate hosts add network latency and independent caches; we would not expect them to behave better.
- Other servers. We tested Linux nfsd and Samba 4.15.13. NAS firmware, Windows file servers, SMB with POSIX extensions and the
forcemandatorylockor cache options were not tested. - A share mounted locally on the NAS. An app running on TrueNAS or Unraid with its config on a local dataset of that same machine is on local storage. The share problem starts when a second machine mounts it.
What to do next#
Find every bind mount whose source is a network path and check whether its folder contains a .db or .sqlite file. Move those to local disk first, then back them up properly from there, which Backing up a running database does for SQLite without stopping the app. If you are deciding where apps should run in the first place, Proxmox vs TrueNAS covers the setup where this question usually comes up: a NAS holding the data and a separate box running the apps.
Questions#
Does adding nobrl or nolock fix SQLite database is locked?
It removes the error and the protection with it. With nobrl on SMB or nolock on NFS, locks are kept by the client kernel and never reach the server. One container on one host then works. A second writer on another mount gets no exclusion: in our tests both writers proceeded, and afterwards one write was missing while PRAGMA integrity_check still said ok. The Servarr wiki's advice to set these options is about the media mount, not the /config folder.
Can SQLite WAL mode work on NFS?
From one client, it switched on and worked in our tests on NFSv3 and NFSv4.2. The SQLite documentation says WAL does not work over a network filesystem because every process must share a small amount of memory, which processes on separate machines cannot do. With two NFS clients in WAL mode, the reader that had not written last got disk I/O error and its next write failed. Vaultwarden's wiki says some network filesystems fail outright with Failed to turn on WAL.
Is NFSv4 safe for a SQLite database?
Safer than SMB with default options, not safe enough to recommend. With locking working, two NFS clients excluded each other correctly in our tests and the second writer got an honest database is locked. SQLite's own FAQ says fcntl locking is broken on many NFS implementations, the NAS side is outside your control, and WAL still cannot be shared between hosts. Jellyfin, the Servarr apps, Uptime Kuma and Audiobookshelf all tell you to keep the database local.
Why does Jellyfin say database is locked on TrueNAS?
Usually because the /config folder is a share from the TrueNAS box, mounted over SMB or NFS into the machine running Jellyfin. Jellyfin's storage documentation says the database should be stored locally and not on a network storage device. Move /config to a local disk on the Jellyfin host, or run Jellyfin on TrueNAS itself with /config on a local dataset, and leave the media library on the share.
How do I move an existing SQLite config off the network share?
Stop the container so nothing has the database open, copy the whole config folder including any -wal and -shm files to local disk, and run sqlite3 app.db 'PRAGMA integrity_check;' on the copy. If it prints ok, point the bind mount at the new folder and start the container. Copying while the app runs can capture a torn write. Check ownership afterwards: the copy may need to belong to the container's uid.
Sources#
- SQLite FAQ, question 5 (fcntl locking broken on many NFS implementations)
- SQLite, Write-Ahead Logging (WAL does not work over a network filesystem)
- SQLite, How To Corrupt An SQLite Database File (file locking problems)
- mount.cifs(8) manual page, nobrl option
- Jellyfin documentation, Storage
- Servarr wiki, Sonarr FAQ (database on a network drive)
- Jellyfin forum, SQLite Error 5: database is locked (config on a CIFS share)
- Uptime Kuma README (NFS not supported)
- Audiobookshelf documentation, Docker install (network mounts)
- Vaultwarden wiki, Running without WAL enabled
- Grafana documentation, configure Grafana ([database] section)
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.