Store cook log DB in writable data dir, not read-only recipes mount
Build and Deploy / build-and-deploy (push) Successful in 2m43s
Build and Deploy / build-and-deploy (push) Successful in 2m43s
The cook log SQLite DB was created at recipeDir/cook-log.db. In the Docker deployment /recipes is mounted read-only, so SQLite could never create the file there — the cook log has never worked in the container. The writable /data volume was mounted but unused. - initDb now creates the DB's parent directory if missing, so startup initialization is self-contained and idempotent. - app takes a dataDir and places cook-log.db there. - Add optional --data-dir flag (defaults to the recipe dir, preserving local-dev behavior); Docker CMD passes --data-dir /data. Verified end-to-end as root against a read-only recipe dir and a non-existent data dir: the dir and DB are created and cook-log POST/GET round-trips. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -32,4 +32,6 @@ WORKDIR /recipes
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["/usr/local/bin/roux-server", "--host", "0.0.0.0", "--config-file", "/roux/config.yaml", "--port", "8080", "--recipe-dir", "/recipes"]
|
||||
# The cook log DB is written to /data (a writable volume); /recipes is mounted
|
||||
# read-only, so the DB cannot live there.
|
||||
CMD ["/usr/local/bin/roux-server", "--host", "0.0.0.0", "--config-file", "/roux/config.yaml", "--port", "8080", "--recipe-dir", "/recipes", "--data-dir", "/data"]
|
||||
|
||||
+18
-1
@@ -5,6 +5,7 @@ Starts a Warp HTTP server and serves recipe content.
|
||||
-}
|
||||
module Main (main) where
|
||||
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.String (fromString)
|
||||
import qualified Network.Wai.Handler.Warp as Warp
|
||||
import qualified Options.Applicative as Opts
|
||||
@@ -21,6 +22,7 @@ data Options = Options
|
||||
{ optHost :: String
|
||||
, optPort :: Int
|
||||
, optRecipeDir :: FilePath
|
||||
, optDataDir :: Maybe FilePath
|
||||
, optConfigFile :: FilePath
|
||||
}
|
||||
|
||||
@@ -51,6 +53,16 @@ optionsParser =
|
||||
<> Opts.value "recipes"
|
||||
<> Opts.showDefault
|
||||
)
|
||||
<*> Opts.optional
|
||||
( Opts.strOption
|
||||
( Opts.long "data-dir"
|
||||
<> Opts.metavar "DIR"
|
||||
<> Opts.help
|
||||
"Writable directory for app state (the cook log DB). \
|
||||
\Created on startup if missing. Defaults to the recipe directory. \
|
||||
\Set this when the recipe directory is read-only (e.g. the Docker deployment mounts recipes read-only and passes --data-dir /data)."
|
||||
)
|
||||
)
|
||||
<*> Opts.strOption
|
||||
( Opts.long "config-file"
|
||||
<> Opts.metavar "FILE"
|
||||
@@ -78,9 +90,14 @@ main = do
|
||||
(fromString (optHost opts))
|
||||
Warp.defaultSettings
|
||||
|
||||
-- Cook log DB (and any future state) lives here; falls back to the recipe
|
||||
-- directory when --data-dir is not given.
|
||||
let dataDir = fromMaybe (optRecipeDir opts) (optDataDir opts)
|
||||
|
||||
putStrLn $ "[roux] scanning recipes from " <> optRecipeDir opts
|
||||
putStrLn $ "[roux] listening on " <> optHost opts <> ":" <> show (optPort opts)
|
||||
putStrLn $ "[roux] config file: " <> optConfigFile opts
|
||||
putStrLn $ "[roux] data dir: " <> dataDir
|
||||
|
||||
-- Load config (file may be missing; LLM features disabled gracefully)
|
||||
configResult <- Config.loadConfig (optConfigFile opts)
|
||||
@@ -90,5 +107,5 @@ main = do
|
||||
case configResult of
|
||||
Left err -> putStrLn $ "[roux] warning: " <> err <> " — LLM features disabled"
|
||||
Right _ -> pure ()
|
||||
waiApp <- Roux.app (optRecipeDir opts) config
|
||||
waiApp <- Roux.app (optRecipeDir opts) dataDir config
|
||||
Warp.runSettings settings waiApp
|
||||
|
||||
@@ -5,5 +5,7 @@ services:
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
volumes:
|
||||
# Cooklang source of truth, managed externally — mounted read-only.
|
||||
- /srv/roux/recipes:/recipes:ro
|
||||
# Writable app state (the cook log SQLite DB); see --data-dir in the Dockerfile CMD.
|
||||
- /srv/roux/data:/data
|
||||
|
||||
+12
-3
@@ -2,8 +2,8 @@
|
||||
|
||||
{- | Cook log: track which recipes were cooked when, with optional notes.
|
||||
|
||||
Uses sqlite-simple for persistence. The database file lives alongside the
|
||||
recipe directory as @cook-log.db@.
|
||||
Uses sqlite-simple for persistence. The database file lives in the writable
|
||||
data directory as @cook-log.db@ (see 'initDb').
|
||||
-}
|
||||
module Roux.CookLog (
|
||||
CookEntry (..),
|
||||
@@ -22,6 +22,8 @@ import Data.Time.Clock (getCurrentTime)
|
||||
import qualified Data.Time.Format as Time
|
||||
import Database.SQLite.Simple (Connection, FromRow (..), Only (..), Query)
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
import System.Directory (createDirectoryIfMissing)
|
||||
import System.FilePath (takeDirectory)
|
||||
|
||||
-- | A single cooking log entry.
|
||||
data CookEntry = CookEntry
|
||||
@@ -70,9 +72,16 @@ instance FromJSON CookEntry where
|
||||
Nothing -> fail "invalid date"
|
||||
_ -> fail "expected YYYY-MM-DD"
|
||||
|
||||
-- | Open or create the DB, ensuring the table exists.
|
||||
{- | Open or create the DB, ensuring the containing directory, the DB file, and
|
||||
the table all exist. Safe to call on every startup: 'createDirectoryIfMissing'
|
||||
and @CREATE TABLE IF NOT EXISTS@ are both no-ops when the targets already exist,
|
||||
and SQLite creates the file itself on first open. The parent directory must be
|
||||
writable — in the Docker deployment this is the @/data@ volume, not the
|
||||
read-only @/recipes@ mount.
|
||||
-}
|
||||
initDb :: FilePath -> IO Connection
|
||||
initDb dbPath = do
|
||||
createDirectoryIfMissing True (takeDirectory dbPath)
|
||||
conn <- SQL.open dbPath
|
||||
SQL.execute_ conn createTableSQL
|
||||
pure conn
|
||||
|
||||
+10
-6
@@ -74,12 +74,15 @@ readRequestBody req = do
|
||||
then pure (reverse acc)
|
||||
else gather (chunk : acc)
|
||||
|
||||
{- | Build the WAI 'Application' given a path to the recipe directory and config.
|
||||
{- | Build the WAI 'Application' given the recipe directory, a writable data
|
||||
directory, and config.
|
||||
|
||||
Recipes are scanned at startup and then watched live via fsnotify.
|
||||
Recipes are scanned at startup and then watched live via fsnotify. The cook log
|
||||
database is created (if absent) in @dataDir@, which must be writable — the
|
||||
recipe directory may be mounted read-only (as it is in the Docker deployment).
|
||||
-}
|
||||
app :: FilePath -> RouxConfig -> IO Wai.Application
|
||||
app recipeDir config = do
|
||||
app :: FilePath -> FilePath -> RouxConfig -> IO Wai.Application
|
||||
app recipeDir dataDir config = do
|
||||
putStrLn $ "[roux] scanning recipes from " <> recipeDir
|
||||
recipes <- Idx.scanRecipes recipeDir
|
||||
let count = length recipes
|
||||
@@ -101,8 +104,9 @@ app recipeDir config = do
|
||||
forkIO $
|
||||
watchRecipes absDir state changeLog `catch` \e ->
|
||||
putStrLn $ "[roux] watcher thread crashed: " <> show (e :: SomeException)
|
||||
-- Initialize cook log database
|
||||
let dbPath = recipeDir </> "cook-log.db"
|
||||
-- Initialize cook log database in the writable data directory (initDb
|
||||
-- creates the directory and file if they are not already present).
|
||||
let dbPath = dataDir </> "cook-log.db"
|
||||
db <- CookLog.initDb dbPath
|
||||
putStrLn $ "[roux] cook log DB: " <> dbPath
|
||||
pure (router absDir config state changeLog db)
|
||||
|
||||
Reference in New Issue
Block a user