Keep the database in ./data and bind-mount it
SQLite writes three files — the database plus its -wal and -shm companions. Putting them in one directory means a deployment mounts a single path and a backup copies a single directory. - FOODSTER_DB defaults to ./data/foodster.db, and openDB creates the parent directory on startup rather than failing on a fresh checkout - compose bind-mounts ./data instead of using a named volume, so the file can be listed, copied and opened with any sqlite client without going through the container engine - the image runs as UID 65534, which cannot write to a host directory owned by someone else, so compose now sets user: from FOODSTER_UID/FOODSTER_GID - `make up` creates ./data first: left to the engine it appears root-owned and the app silently cannot write to it
This commit is contained in:
@@ -11,6 +11,12 @@ FOODSTER_PASSWORD=changeme
|
||||
# Host port to publish on.
|
||||
FOODSTER_PORT=8080
|
||||
|
||||
# The database lives in ./data, bind-mounted into the container. These must
|
||||
# match whoever owns that directory on the host, or the container cannot
|
||||
# write to it. `id -u` and `id -g` will tell you.
|
||||
FOODSTER_UID=1000
|
||||
FOODSTER_GID=1000
|
||||
|
||||
# Used for every calendar-day calculation. Set it in development too: under
|
||||
# UTC the date rolls over three hours late, which is exactly when dinner
|
||||
# gets logged.
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@
|
||||
# Generated by `templ generate` during the container build.
|
||||
*_templ.go
|
||||
|
||||
# Local database
|
||||
# The database and its SQLite companions.
|
||||
/data/
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
@@ -31,10 +31,8 @@ build: generate ## Build ./foodster
|
||||
CGO_ENABLED=0 go build -trimpath \
|
||||
-ldflags="-s -w -X main.version=dev" -o $(BIN) $(PKG)
|
||||
|
||||
run: generate ## Run locally on :8080
|
||||
FOODSTER_PASSWORD=$${FOODSTER_PASSWORD:-dev} \
|
||||
FOODSTER_DB=$${FOODSTER_DB:-./foodster.db} \
|
||||
go run $(PKG)
|
||||
run: generate ## Run locally on :8080 (database in ./data)
|
||||
FOODSTER_PASSWORD=$${FOODSTER_PASSWORD:-dev} go run $(PKG)
|
||||
|
||||
seed: ## Import a dish bundle (SEED=seeds/testi.json)
|
||||
go run $(PKG) -import $(SEED)
|
||||
@@ -84,6 +82,7 @@ push: ## Push the newest tag and :latest
|
||||
release: image push ## Build, tag and push in one go
|
||||
|
||||
up: ## Start the stack
|
||||
@mkdir -p data # or the engine creates it root-owned and the app cannot write
|
||||
$(COMPOSE) up -d
|
||||
|
||||
down: ## Stop the stack
|
||||
|
||||
@@ -342,13 +342,19 @@ on the server and run with Docker Compose.
|
||||
static binary; the runtime stage is `FROM scratch` holding only that
|
||||
binary, running as UID 65534.
|
||||
- **Compose**: a single service. No database container — SQLite lives at
|
||||
`/data/foodster.db` on a named volume. `restart: unless-stopped`.
|
||||
`/data/foodster.db`, bind-mounted from `./data` on the host rather than
|
||||
kept in a named volume, so the file can be listed and copied without going
|
||||
through the container engine. Backup is `cp -r data`. Because the image
|
||||
runs as UID 65534, compose sets `user:` from `FOODSTER_UID`/`FOODSTER_GID`
|
||||
to match whoever owns that directory. `restart: unless-stopped`.
|
||||
- **Configuration**, entirely through environment variables (see
|
||||
`.env.example`):
|
||||
- `FOODSTER_REPO` and `FOODSTER_TAG` — image coordinates.
|
||||
- `FOODSTER_PASSWORD` — the shared password. Required; the app refuses to
|
||||
start without it.
|
||||
- `FOODSTER_DB` — database file path, default `/data/foodster.db`.
|
||||
- `FOODSTER_DB` — database file path, default `./data/foodster.db`. The
|
||||
directory is created on startup if missing.
|
||||
- `FOODSTER_UID` / `FOODSTER_GID` — host owner of `./data`.
|
||||
- `TZ` — default `Europe/Helsinki`.
|
||||
- The registry hostname exists only in `.env`, which is gitignored, because
|
||||
§11 leaves open the possibility of publishing this repository.
|
||||
|
||||
@@ -105,7 +105,8 @@ Everything is environment variables. `.env` is gitignored; start from
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `FOODSTER_PASSWORD` | *required* | Shared password. The app will not start without it. |
|
||||
| `FOODSTER_DB` | `/data/foodster.db` | SQLite file path. |
|
||||
| `FOODSTER_DB` | `./data/foodster.db` | SQLite file path; the directory is created if missing. |
|
||||
| `FOODSTER_UID` / `FOODSTER_GID` | `1000` | Host owner of `./data`, for the bind mount. |
|
||||
| `TZ` | `Europe/Helsinki` | Used for every calendar-day calculation. |
|
||||
| `FOODSTER_REPO` | *required to build* | Image repository, no tag. |
|
||||
| `FOODSTER_TAG` | `latest` | Tag to run under compose. |
|
||||
@@ -129,8 +130,13 @@ Versions are CalVer — `vYYYYMMDD-N`, where `N` is the Nth build that day. The
|
||||
running version is served at `GET /healthz`, which is the one route outside
|
||||
authentication.
|
||||
|
||||
There is no database container. SQLite lives on a named volume, so a backup
|
||||
is a file copy.
|
||||
There is no database container. SQLite lives in `./data`, bind-mounted into
|
||||
the container, so a backup is `cp -r data` and you can inspect the file with
|
||||
any sqlite client without going through the engine.
|
||||
|
||||
That directory must exist and be owned by the user compose runs as — `make up`
|
||||
creates it, and `FOODSTER_UID`/`FOODSTER_GID` in `.env` tell the container who
|
||||
that is. Get them from `id -u` and `id -g`.
|
||||
|
||||
## Security
|
||||
|
||||
|
||||
+11
-1
@@ -18,6 +18,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -36,7 +37,10 @@ var version = "dev"
|
||||
const (
|
||||
listenAddr = ":8080"
|
||||
defaultTZ = "Europe/Helsinki"
|
||||
defaultDB = "./foodster.db"
|
||||
// Everything SQLite writes — the database plus its -wal and -shm
|
||||
// companions — lives in one directory, so a deployment mounts a single
|
||||
// path and a backup copies a single directory.
|
||||
defaultDB = "./data/foodster.db"
|
||||
|
||||
// failDelay throttles password guessing.
|
||||
// ponytail: a fixed sleep is enough for a LAN-only app; swap in
|
||||
@@ -108,6 +112,12 @@ func run() error {
|
||||
|
||||
// openDB opens the SQLite file and brings its schema up to date.
|
||||
func openDB(path string) (*sql.DB, error) {
|
||||
if dir := filepath.Dir(path); dir != "." && dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
dsn := "file:" + path +
|
||||
"?_pragma=journal_mode(WAL)" +
|
||||
"&_pragma=foreign_keys(ON)" +
|
||||
|
||||
+6
-4
@@ -2,6 +2,11 @@ services:
|
||||
app:
|
||||
image: ${FOODSTER_REPO:?set FOODSTER_REPO in .env}:${FOODSTER_TAG:-latest}
|
||||
restart: unless-stopped
|
||||
# A bind mount rather than a named volume: the database sits in ./data on
|
||||
# the host, where it can be listed, copied and backed up without going
|
||||
# through the container engine. The image runs as UID 65534, so the
|
||||
# container has to be told which host user owns that directory.
|
||||
user: "${FOODSTER_UID:-1000}:${FOODSTER_GID:-1000}"
|
||||
ports:
|
||||
- "${FOODSTER_PORT:-8080}:8080"
|
||||
environment:
|
||||
@@ -9,7 +14,4 @@ services:
|
||||
FOODSTER_DB: /data/foodster.db
|
||||
TZ: ${TZ:-Europe/Helsinki}
|
||||
volumes:
|
||||
- foodster-data:/data
|
||||
|
||||
volumes:
|
||||
foodster-data:
|
||||
- ./data:/data
|
||||
|
||||
Reference in New Issue
Block a user