Troubleshooting
PostgreSQL & databases

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.

Why writes failApp userwww-data or deployDB file ownersomeone elseDirectory lockedcannot add journalReadonly errorwrites refusedSQLite needs write access to the file and permission to create files beside it.
  1. Find the process user (ps -o user= -p $(pgrep -f gunicorn | head -1)) and the file owner (ls -l db.sqlite3).
  2. Give that user ownership of the file and the directory: sudo chown deploy:deploy /srv/app/data /srv/app/data/db.sqlite3.
  3. Keep the database in its own writable directory, not the code directory.
  4. Use an absolute path in DATABASES["default"]["NAME"], built from BASE_DIR or an environment variable.
  5. 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.

what ls showsWhat you findCause and fixfile owned by rootsudo migrate createdit; chown to app userdirectory not writablejournal cannot be created; own the dirrelative NAME pathdifferent cwd perprocess; use absolutefile inside the imageread-only or lostlayer; mount a volumeThe file and its directory both need write access for the app userdjango-deployment.com
Permission on the file alone is not enough.

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.

what lives in the data directoryPurposeCreated whendb.sqlite3the databasefirst migratedb.sqlite3-journalrollback journaleach write (default)db.sqlite3-walwrite-ahead logWAL mode writesdb.sqlite3-shmshared memory indexWAL modeSQLite creates these beside the database, so the directory must be writabledjango-deployment.com
Back up the WAL file with the database, or checkpoint first.

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

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.

Keep going