Fix Django SQLite "Attempt to Write a Readonly Database"
Fix Django SQLite errors attempt to write a readonly database and unable to open database file in production: give the app user write access to the database file and its directory, use an absolute path, and handle Docker volumes and read-only images.
Problem statement
Pages that only read data work, but logging in, saving a form, or running a migration fails:
django.db.utils.OperationalError: attempt to write a readonly database
or, before any query runs:
django.db.utils.OperationalError: unable to open database file
SQLite stores everything in one file, plus temporary journal or WAL files next to it. The process running Django must be able to write the database file and create files in its directory. Under Gunicorn, systemd, Apache, or Docker, that process often runs as a different user than the one who created the file, so reads succeed and writes fail.
- Find the process user (
ps -o user= -p $(pgrep -f gunicorn | head -1)) and the file owner (ls -l db.sqlite3). - Give that user ownership of the file and the directory:
sudo chown deploy:deploy /srv/app/data /srv/app/data/db.sqlite3. - Keep the database in its own writable directory, not the code directory.
- Use an absolute path in
DATABASES["default"]["NAME"], built fromBASE_DIRor an environment variable. - In Docker, mount a volume for the database directory, owned by the container's user.
Step-by-step solution
1. Compare users and permissions
ls -l /srv/app/data/
ls -ld /srv/app/data
systemctl show -p User myapp
If db.sqlite3 is owned by root because someone ran sudo python manage.py migrate, the deploy user can read it (mode 644) but not write it. If the directory is not writable, SQLite cannot create db.sqlite3-journal or db.sqlite3-wal, and writes fail even when the file itself is writable.
2. Give the app user the directory
sudo install -d -o deploy -g deploy -m 750 /srv/app/data
sudo mv /srv/app/db.sqlite3 /srv/app/data/
sudo chown deploy:deploy /srv/app/data/db.sqlite3*
Keeping the database in a dedicated directory means you can lock down the code directory while the data directory stays writable.
3. Use an absolute path
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.environ.get("SQLITE_PATH", str(BASE_DIR / "data" / "db.sqlite3")),
}
}
A relative path like "db.sqlite3" resolves against the current working directory, which differs between your shell, systemd, cron, and Celery. One process then creates a new empty database somewhere unexpected, or fails with "unable to open database file" when that directory does not exist or is not writable.
4. Docker volumes
Inside an image, the database file is either read-only (if the filesystem is mounted read-only) or lost on every redeploy. Mount a volume for the data directory and make it writable by the container user:
services:
web:
image: myapp:latest
user: "1000:1000"
environment:
SQLITE_PATH: /data/db.sqlite3
volumes:
- sqlite-data:/data
volumes:
sqlite-data:
In the Dockerfile, create /data owned by the app user before USER app, so a fresh named volume inherits that ownership.
5. Run management commands as the app user
sudo -u deploy /srv/app/.venv/bin/python /srv/app/manage.py migrate
docker compose exec web python manage.py migrate # runs as the container user
Never run migrate with sudo on a SQLite project; it recreates the problem.
Explanation
SQLite opens the database file read-write when the process has write permission and read-only otherwise, without failing at open time. The first write then fails with "attempt to write a readonly database". Writes also require creating a journal or WAL file in the same directory, which needs write and execute permission on the directory. "Unable to open database file" is the stricter failure: SQLite cannot even open or create the file, typically because the directory does not exist or the path is relative to an unexpected working directory.
Edge cases / notes
- SELinux. On RHEL-family systems, a correctly owned file can still be denied for a confined service; check the audit log.
- Network filesystems. SQLite over NFS or SMB has locking problems and can report read-only errors; keep the database on local disk.
- Multiple processes. Even with correct permissions, concurrent writers can hit "database is locked"; see the related fix.
Next steps
- Run SQLite properly with Run Django on SQLite in Production.
- For concurrency errors, see Fix SQLite Database Is Locked in Django.
- Outgrowing SQLite? Read Migrate Django From SQLite to PostgreSQL.
FAQ
Why can Django read but not write?
The app user has read permission on the file but not write permission on the file or its directory.
Why does SQLite need the directory to be writable?
It creates journal or WAL files next to the database during writes.
Is it OK to chmod 666 the database?
It hides the problem and lets any user modify your data. Give ownership to the app user instead.
Should I use SQLite in production at all?
For small, mostly single-server apps it can work well. For multiple servers or heavy write concurrency, PostgreSQL is a better fit.