diff --git a/combined/cmd/config.go b/combined/cmd/config.go index f52d38ccf..85664d0d2 100644 --- a/combined/cmd/config.go +++ b/combined/cmd/config.go @@ -7,6 +7,7 @@ import ( "net/netip" "os" "path" + "path/filepath" "strings" "time" @@ -172,7 +173,8 @@ type RelaysConfig struct { type StoreConfig struct { Engine string `yaml:"engine"` EncryptionKey string `yaml:"encryptionKey"` - DSN string `yaml:"dsn"` // Connection string for postgres or mysql engines + DSN string `yaml:"dsn"` // Connection string for postgres or mysql engines + File string `yaml:"file"` // SQLite database file path (optional, defaults to dataDir) } // ReverseProxyConfig contains reverse proxy settings @@ -568,6 +570,12 @@ func (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.Emb } } else { authStorageFile = path.Join(mgmt.DataDir, "idp.db") + if c.Server.AuthStore.File != "" { + authStorageFile = c.Server.AuthStore.File + if !filepath.IsAbs(authStorageFile) { + authStorageFile = filepath.Join(mgmt.DataDir, authStorageFile) + } + } } cfg := &idp.EmbeddedIdPConfig{ diff --git a/combined/cmd/root.go b/combined/cmd/root.go index 00edcb5d4..153260341 100644 --- a/combined/cmd/root.go +++ b/combined/cmd/root.go @@ -140,6 +140,9 @@ func initializeConfig() error { os.Setenv("NB_STORE_ENGINE_MYSQL_DSN", dsn) } } + if file := config.Server.Store.File; file != "" { + os.Setenv("NB_STORE_ENGINE_SQLITE_FILE", file) + } if engine := config.Server.ActivityStore.Engine; engine != "" { engineLower := strings.ToLower(engine) @@ -151,6 +154,9 @@ func initializeConfig() error { os.Setenv("NB_ACTIVITY_EVENT_POSTGRES_DSN", dsn) } } + if file := config.Server.ActivityStore.File; file != "" { + os.Setenv("NB_ACTIVITY_EVENT_SQLITE_FILE", file) + } log.Infof("Starting combined NetBird server") logConfig(config) diff --git a/combined/cmd/token.go b/combined/cmd/token.go index 9393c6c46..550480062 100644 --- a/combined/cmd/token.go +++ b/combined/cmd/token.go @@ -42,6 +42,9 @@ func withTokenStore(cmd *cobra.Command, fn func(ctx context.Context, s store.Sto os.Setenv("NB_STORE_ENGINE_MYSQL_DSN", dsn) } } + if file := cfg.Server.Store.File; file != "" { + os.Setenv("NB_STORE_ENGINE_SQLITE_FILE", file) + } datadir := cfg.Management.DataDir engine := types.Engine(cfg.Management.Store.Engine) diff --git a/combined/config.yaml.example b/combined/config.yaml.example index f81973c6b..dce658d89 100644 --- a/combined/config.yaml.example +++ b/combined/config.yaml.example @@ -103,16 +103,19 @@ server: engine: "sqlite" # sqlite, postgres, or mysql dsn: "" # Connection string for postgres or mysql encryptionKey: "" + # file: "" # Custom SQLite file path (optional, defaults to {dataDir}/store.db) # Activity events store configuration (optional, defaults to sqlite in dataDir) # activityStore: # engine: "sqlite" # sqlite or postgres # dsn: "" # Connection string for postgres + # file: "" # Custom SQLite file path (optional, defaults to {dataDir}/events.db) # Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db) # authStore: # engine: "sqlite3" # sqlite3 or postgres # dsn: "" # Connection string for postgres (e.g., "host=localhost port=5432 user=postgres password=postgres dbname=netbird_idp sslmode=disable") + # file: "" # Custom SQLite file path (optional, defaults to {dataDir}/idp.db) # Reverse proxy settings (optional) # reverseProxy: diff --git a/management/server/activity/store/sql_store.go b/management/server/activity/store/sql_store.go index db614d0cd..73e8e295c 100644 --- a/management/server/activity/store/sql_store.go +++ b/management/server/activity/store/sql_store.go @@ -249,7 +249,15 @@ func initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) { switch storeEngine { case types.SqliteStoreEngine: - dialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB)) + dbFile := eventSinkDB + if envFile, ok := os.LookupEnv("NB_ACTIVITY_EVENT_SQLITE_FILE"); ok && envFile != "" { + dbFile = envFile + } + connStr := dbFile + if !filepath.IsAbs(dbFile) { + connStr = filepath.Join(dataDir, dbFile) + } + dialector = sqlite.Open(connStr) case types.PostgresStoreEngine: dsn, ok := os.LookupEnv(postgresDsnEnv) if !ok { diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 89fe22cec..04045f226 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -2728,14 +2728,28 @@ func (s *SqlStore) GetStoreEngine() types.Engine { // NewSqliteStore creates a new SQLite store. func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) { - storeStr := fmt.Sprintf("%s?cache=shared", storeSqliteFileName) - if runtime.GOOS == "windows" { - // Vo avoid `The process cannot access the file because it is being used by another process` on Windows - storeStr = storeSqliteFileName + storeFile := storeSqliteFileName + if envFile, ok := os.LookupEnv("NB_STORE_ENGINE_SQLITE_FILE"); ok && envFile != "" { + storeFile = envFile } - file := filepath.Join(dataDir, storeStr) - db, err := gorm.Open(sqlite.Open(file), getGormConfig()) + // Separate file path from any SQLite URI query parameters (e.g., "store.db?mode=rwc") + filePath, query, hasQuery := strings.Cut(storeFile, "?") + + connStr := filePath + if !filepath.IsAbs(filePath) { + connStr = filepath.Join(dataDir, filePath) + } + + // Append query parameters: user-provided take precedence, otherwise default to cache=shared on non-Windows + if hasQuery { + connStr += "?" + query + } else if runtime.GOOS != "windows" { + // To avoid `The process cannot access the file because it is being used by another process` on Windows + connStr += "?cache=shared" + } + + db, err := gorm.Open(sqlite.Open(connStr), getGormConfig()) if err != nil { return nil, err }