Skip to content

Commit 1db337d

Browse files
committed
fix(database): use WAL mode to prevent readonly errors in restricted Docker
Enable SQLite WAL (Write-Ahead Logging) mode by default to reduce filesystem operations and improve compatibility with Docker containers running with restricted capabilities (cap_drop: [ALL]). Changes: - Add journal_mode=WAL, temp_store=MEMORY, synchronous=NORMAL PRAGMAs - Add web.database.journal_mode config option for flexibility - Improve error messages for readonly database errors with actionable guidance - Document new configuration in example.scrutiny.yaml Fixes #25 Upstream: AnalogJ#772
1 parent 7f4bceb commit 1db337d

File tree

4 files changed

+58
-6
lines changed

4 files changed

+58
-6
lines changed

example.scrutiny.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ web:
3030
database:
3131
# can also set absolute path here
3232
location: /opt/scrutiny/config/scrutiny.db
33+
# SQLite journal mode: WAL (default), DELETE, TRUNCATE, PERSIST, MEMORY, OFF
34+
# WAL mode is recommended for Docker containers with restricted capabilities (cap_drop: [ALL])
35+
# If you experience database issues with WAL mode on network filesystems (NFS), try DELETE mode
36+
# journal_mode: WAL
3337
src:
3438
# the location on the filesystem where scrutiny javascript + css is located
3539
frontend:

webapp/backend/pkg/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func (c *configuration) Init() error {
3636
c.SetDefault("web.listen.basepath", "")
3737
c.SetDefault("web.src.frontend.path", "/opt/scrutiny/web")
3838
c.SetDefault("web.database.location", "/opt/scrutiny/config/scrutiny.db")
39+
c.SetDefault("web.database.journal_mode", "WAL")
3940

4041
c.SetDefault("log.level", "INFO")
4142
c.SetDefault("log.file", "")

webapp/backend/pkg/database/scrutiny_repository.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import (
55
"crypto/tls"
66
"encoding/json"
77
"fmt"
8+
"io/ioutil"
9+
"net/http"
10+
"net/url"
11+
"strings"
12+
"time"
13+
814
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
915
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
1016
"github.com/glebarez/sqlite"
@@ -13,10 +19,6 @@ import (
1319
"github.com/influxdata/influxdb-client-go/v2/domain"
1420
"github.com/sirupsen/logrus"
1521
"gorm.io/gorm"
16-
"io/ioutil"
17-
"net/http"
18-
"net/url"
19-
"time"
2022
)
2123

2224
const (
@@ -74,15 +76,37 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
7476
// https://rsqlite.r-dbi.org/reference/sqlitesetbusyhandler
7577
// retrying for 30000 milliseconds, 30seconds - this would be unreasonable for a distributed multi-tenant application,
7678
// but should be fine for local usage.
79+
//
80+
// WAL (Write-Ahead Logging) mode is used by default to reduce filesystem operations and improve
81+
// compatibility with Docker containers running with restricted capabilities (cap_drop: [ALL]).
82+
// fixes #772 (upstream), #25
83+
// https://www.sqlite.org/wal.html
84+
journalMode := appConfig.GetString("web.database.journal_mode")
85+
if journalMode == "" {
86+
journalMode = "WAL"
87+
}
88+
7789
pragmaStr := sqlitePragmaString(map[string]string{
7890
"busy_timeout": "30000",
91+
"journal_mode": journalMode,
92+
"temp_store": "MEMORY",
93+
"synchronous": "NORMAL",
7994
})
8095
database, err := gorm.Open(sqlite.Open(appConfig.GetString("web.database.location")+pragmaStr), &gorm.Config{
8196
//TODO: figure out how to log database queries again.
8297
//Logger: logger
8398
DisableForeignKeyConstraintWhenMigrating: true,
8499
})
85100
if err != nil {
101+
if strings.Contains(err.Error(), "readonly database") ||
102+
strings.Contains(err.Error(), "attempt to write") {
103+
return nil, fmt.Errorf("Database write error: %v\n\n"+
104+
"This often occurs when running Docker with 'cap_drop: [ALL]'.\n"+
105+
"Solutions:\n"+
106+
"1. Ensure database directory has write permissions\n"+
107+
"2. If using cap_drop, add: cap_add: [CHOWN, DAC_OVERRIDE, FOWNER]\n"+
108+
"3. Set 'web.database.journal_mode: DELETE' in scrutiny.yaml if WAL causes issues", err)
109+
}
86110
return nil, fmt.Errorf("Failed to connect to database! - %v", err)
87111
}
88112
globalLogger.Infof("Successfully connected to scrutiny sqlite db: %s\n", appConfig.GetString("web.database.location"))

webapp/backend/pkg/database/scrutiny_repository_migrations.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"strconv"
8+
"strings"
89
"time"
910

1011
"github.com/analogj/scrutiny/webapp/backend/pkg"
@@ -436,7 +437,18 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
436437
})
437438

438439
if err := m.Migrate(); err != nil {
439-
sr.logger.Errorf("Database migration failed with error. \n Please open a github issue at https://github.com/AnalogJ/scrutiny and attach a copy of your scrutiny.db file. \n %v", err)
440+
if strings.Contains(err.Error(), "readonly database") ||
441+
strings.Contains(err.Error(), "attempt to write") {
442+
sr.logger.Errorf("Database migration failed: unable to write to database.\n\n"+
443+
"This error commonly occurs in Docker containers with restricted capabilities.\n"+
444+
"Solutions:\n"+
445+
"1. Check file permissions on the database directory\n"+
446+
"2. If using 'cap_drop: [ALL]', add necessary capabilities back\n"+
447+
"3. Verify the volume mount has correct ownership\n\n"+
448+
"Original error: %v", err)
449+
} else {
450+
sr.logger.Errorf("Database migration failed with error.\nPlease open a github issue at https://github.com/Starosdev/scrutiny and attach a copy of your scrutiny.db file.\n%v", err)
451+
}
440452
return err
441453
}
442454
sr.logger.Infoln("Database migration completed successfully")
@@ -459,7 +471,18 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
459471
})
460472

461473
if err := gm.Migrate(); err != nil {
462-
sr.logger.Errorf("SQLite global configuration migrations failed with error. \n Please open a github issue at https://github.com/AnalogJ/scrutiny and attach a copy of your scrutiny.db file. \n %v", err)
474+
if strings.Contains(err.Error(), "readonly database") ||
475+
strings.Contains(err.Error(), "attempt to write") {
476+
sr.logger.Errorf("SQLite global configuration migrations failed: unable to write to database.\n\n"+
477+
"This error commonly occurs in Docker containers with restricted capabilities.\n"+
478+
"Solutions:\n"+
479+
"1. Check file permissions on the database directory\n"+
480+
"2. If using 'cap_drop: [ALL]', add necessary capabilities back\n"+
481+
"3. Verify the volume mount has correct ownership\n\n"+
482+
"Original error: %v", err)
483+
} else {
484+
sr.logger.Errorf("SQLite global configuration migrations failed with error.\nPlease open a github issue at https://github.com/Starosdev/scrutiny and attach a copy of your scrutiny.db file.\n%v", err)
485+
}
463486
return err
464487
}
465488
sr.logger.Infoln("SQLite global configuration migrations completed successfully")

0 commit comments

Comments
 (0)