Skip to content

Commit cd449de

Browse files
authored
Merge pull request #69 from GeiserX/fix/sqlite-readonly-viewer
fix: handle read-only SQLite gracefully in viewer containers
2 parents d56baa9 + 869a686 commit cd449de

3 files changed

Lines changed: 41 additions & 13 deletions

File tree

docker-compose.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ services:
155155
LOG_LEVEL: ${LOG_LEVEL:-INFO}
156156

157157
volumes:
158-
- ./data:/data:ro
158+
# SQLite needs write access for WAL journal files (.db-wal, .db-shm).
159+
# Use :ro only if you are using PostgreSQL as the database backend.
160+
- ./data:/data
159161
networks:
160162
- telegram-network
161163
# Security hardening

docs/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ For upgrade instructions, see [Upgrading](#upgrading) at the bottom.
66

77
## [Unreleased]
88

9+
## [6.2.6] - 2026-02-07
10+
11+
### Fixed
12+
13+
- **SQLite viewer crash** — Viewer container failed to start when using SQLite because `PRAGMA journal_mode=WAL` requires write access to create `.db-wal` and `.db-shm` sidecar files. WAL and `create_all` are now wrapped in try/except so the viewer degrades gracefully to default journal mode instead of crashing. (#61)
14+
- **Read-only volume mount** — Removed `:ro` from the viewer volume in `docker-compose.yml` since SQLite WAL needs write access. Added comment explaining when `:ro` is safe (PostgreSQL only).
15+
916
## [6.2.5] - 2026-02-07
1017

1118
### Fixed

src/db/base.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -135,27 +135,46 @@ async def init(self) -> None:
135135
# via entrypoint.sh, so running create_all concurrently would race with
136136
# Alembic migrations and cause deadlocks.
137137
if self._is_sqlite:
138-
async with self.engine.begin() as conn:
139-
await conn.run_sync(lambda sync_conn: Base.metadata.create_all(sync_conn, checkfirst=True))
138+
try:
139+
async with self.engine.begin() as conn:
140+
await conn.run_sync(lambda sync_conn: Base.metadata.create_all(sync_conn, checkfirst=True))
141+
except Exception as e:
142+
# Viewer containers may mount the database read-only — that's fine,
143+
# the backup container is responsible for creating tables.
144+
logger.warning(f"Could not create/verify tables (database may be read-only): {e}")
140145

141146
logger.info(f"Database initialized successfully ({self._db_type()})")
142147

143148
def _setup_sqlite_pragmas(self) -> None:
144-
"""Set up SQLite PRAGMA settings for optimal performance."""
149+
"""Set up SQLite PRAGMA settings for optimal performance.
150+
151+
Gracefully handles read-only databases (e.g., viewer containers with
152+
read-only volume mounts or non-root users without write permissions).
153+
WAL mode requires write access to create .db-wal and .db-shm files;
154+
if that fails the database still works in the default journal mode.
155+
"""
145156

146157
@event.listens_for(self.engine.sync_engine, "connect")
147158
def set_sqlite_pragma(dbapi_connection, connection_record):
148159
cursor = dbapi_connection.cursor()
149-
# WAL mode for better concurrent read/write
150-
cursor.execute("PRAGMA journal_mode=WAL")
151-
# 60 second busy timeout
152-
cursor.execute("PRAGMA busy_timeout=60000")
153-
# Faster than FULL, still safe with WAL
154-
cursor.execute("PRAGMA synchronous=NORMAL")
155-
# 64MB cache for better performance
156-
cursor.execute("PRAGMA cache_size=-64000")
160+
try:
161+
# WAL mode for better concurrent read/write
162+
cursor.execute("PRAGMA journal_mode=WAL")
163+
# Faster than FULL, still safe with WAL
164+
cursor.execute("PRAGMA synchronous=NORMAL")
165+
except Exception:
166+
logger.warning(
167+
"Could not enable WAL mode (database may be read-only). "
168+
"This is expected for viewer containers with read-only mounts."
169+
)
170+
try:
171+
# 60 second busy timeout
172+
cursor.execute("PRAGMA busy_timeout=60000")
173+
# 64MB cache for better performance
174+
cursor.execute("PRAGMA cache_size=-64000")
175+
except Exception:
176+
pass # Read-only PRAGMAs are non-critical
157177
cursor.close()
158-
logger.debug("SQLite PRAGMAs set: WAL mode, 60s timeout, 64MB cache")
159178

160179
def _db_type(self) -> str:
161180
"""Get human-readable database type."""

0 commit comments

Comments
 (0)