attempt to write a readonly database
This page helps you when the auth server can’t write its SQLite database.
What you see
Section titled “What you see”The auth server stops right after it opens the database, and its log has unable to create the database connection, with one of these as its error:
unable to connect to database: Attempt to write a readonly database (SQLITE_READONLY): attempt to write a readonly database (1544), when it can’t write the directory holding the database file, where SQLite creates the files it keeps beside it. That’s the usual case: an auth server that stops cleanly removes those files.unable to migrate the database: unable to clear schema_migrations: attempt to write a readonly database (8), when it can’t write the file, and the new release has a migration to run.
Under Docker Compose, the container restarts and stops again with the same record.
When there’s nothing to migrate, a read-only file doesn’t stop the start, and neither does a read-only directory that still holds the files SQLite keeps beside the database, as it does when the last auth server there didn’t stop cleanly: it was killed, for instance because its stop outlasted the platform’s grace period, it crashed, or its requests or background work outlived its own shutdown timeouts. The auth server starts, and every request that writes to the database fails instead, such as a sign-in, which shows the user an error page. Its error record ends in attempt to write a readonly database (8), and so do the background cleanup’s.
Why it happens
Section titled “Why it happens”The auth server runs as a user that can’t write the database. SQLite needs to write the file and to create the files it keeps beside it in the same directory, goiabada.db-wal and goiabada.db-shm.
The image runs as uid 10001 and gid 10001. Its /data directory belongs to that user, and Docker copies a mount point’s owner into an empty named volume, so a new volume mounted at /data is writable from the first start. A volume whose files belong to another user isn’t:
- The volume was written by a container that ran as root, such as an older image, or a
docker compose runas root. - The volume is mounted somewhere other than
/data, at a path the image doesn’t have. Docker creates that directory owned by root. - It’s a bind mount of a host directory that belongs to another user.
With native binaries, it’s the same story when the service runs as a user other than the one owning the database’s directory.
Kubernetes doesn’t run into this: SQLite isn’t supported there, and the generated manifest mounts no volume.
Fix it
Section titled “Fix it”Give the database’s directory, and every file in it, to the user the auth server runs as.
-
Stop the auth server, from the directory holding your
docker-compose.yml:Terminal window docker compose stop goiabada-authserver -
Change the owner, once, in a throwaway container of the auth server’s service:
Terminal window docker compose run --rm --no-deps --user 0:0 --cap-add CHOWN --entrypoint chown \goiabada-authserver -R 10001:10001 /dataThis runs
chownas root, so it reaches the volume wherever the service mounts it.--cap-add CHOWNis what lets root change the owner in a service that drops every capability. Use the path your file mounts the volume at,/datain the file the setup wizard writes. -
Start everything again:
Terminal window docker compose up -d
Give the directory to the user the service runs as, here goiabada with the database in /var/lib/goiabada:
sudo systemctl stop goiabada-authserversudo chown -R goiabada:goiabada /var/lib/goiabadasudo systemctl start goiabada-authserverWhy it fails at the first write
Section titled “Why it fails at the first write”SQLite opens a file it can’t write read-only rather than refusing it. A read-only database connects, so the auth server only finds out at its first write. On an upgrade, that’s the migration’s first step, recording the version it’s about to apply, which is why the second message names schema_migrations. A directory it can’t write fails sooner, since the database runs in WAL mode and SQLite can’t create its WAL files. When an auth server that didn’t stop cleanly left them there, SQLite opens them read-only too, and the first write is again what fails.
The auth server checks nothing about the file’s owner before it opens it. Nothing is written when a start fails this way, so the start after the fix migrates as usual.