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.
|
# Host port to publish on.
|
||||||
FOODSTER_PORT=8080
|
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
|
# 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
|
# UTC the date rolls over three hours late, which is exactly when dinner
|
||||||
# gets logged.
|
# gets logged.
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
# Generated by `templ generate` during the container build.
|
# Generated by `templ generate` during the container build.
|
||||||
*_templ.go
|
*_templ.go
|
||||||
|
|
||||||
# Local database
|
# The database and its SQLite companions.
|
||||||
|
/data/
|
||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
|
|||||||
@@ -31,10 +31,8 @@ build: generate ## Build ./foodster
|
|||||||
CGO_ENABLED=0 go build -trimpath \
|
CGO_ENABLED=0 go build -trimpath \
|
||||||
-ldflags="-s -w -X main.version=dev" -o $(BIN) $(PKG)
|
-ldflags="-s -w -X main.version=dev" -o $(BIN) $(PKG)
|
||||||
|
|
||||||
run: generate ## Run locally on :8080
|
run: generate ## Run locally on :8080 (database in ./data)
|
||||||
FOODSTER_PASSWORD=$${FOODSTER_PASSWORD:-dev} \
|
FOODSTER_PASSWORD=$${FOODSTER_PASSWORD:-dev} go run $(PKG)
|
||||||
FOODSTER_DB=$${FOODSTER_DB:-./foodster.db} \
|
|
||||||
go run $(PKG)
|
|
||||||
|
|
||||||
seed: ## Import a dish bundle (SEED=seeds/testi.json)
|
seed: ## Import a dish bundle (SEED=seeds/testi.json)
|
||||||
go run $(PKG) -import $(SEED)
|
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
|
release: image push ## Build, tag and push in one go
|
||||||
|
|
||||||
up: ## Start the stack
|
up: ## Start the stack
|
||||||
|
@mkdir -p data # or the engine creates it root-owned and the app cannot write
|
||||||
$(COMPOSE) up -d
|
$(COMPOSE) up -d
|
||||||
|
|
||||||
down: ## Stop the stack
|
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
|
static binary; the runtime stage is `FROM scratch` holding only that
|
||||||
binary, running as UID 65534.
|
binary, running as UID 65534.
|
||||||
- **Compose**: a single service. No database container — SQLite lives at
|
- **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
|
- **Configuration**, entirely through environment variables (see
|
||||||
`.env.example`):
|
`.env.example`):
|
||||||
- `FOODSTER_REPO` and `FOODSTER_TAG` — image coordinates.
|
- `FOODSTER_REPO` and `FOODSTER_TAG` — image coordinates.
|
||||||
- `FOODSTER_PASSWORD` — the shared password. Required; the app refuses to
|
- `FOODSTER_PASSWORD` — the shared password. Required; the app refuses to
|
||||||
start without it.
|
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`.
|
- `TZ` — default `Europe/Helsinki`.
|
||||||
- The registry hostname exists only in `.env`, which is gitignored, because
|
- The registry hostname exists only in `.env`, which is gitignored, because
|
||||||
§11 leaves open the possibility of publishing this repository.
|
§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 |
|
| Variable | Default | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `FOODSTER_PASSWORD` | *required* | Shared password. The app will not start without it. |
|
| `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. |
|
| `TZ` | `Europe/Helsinki` | Used for every calendar-day calculation. |
|
||||||
| `FOODSTER_REPO` | *required to build* | Image repository, no tag. |
|
| `FOODSTER_REPO` | *required to build* | Image repository, no tag. |
|
||||||
| `FOODSTER_TAG` | `latest` | Tag to run under compose. |
|
| `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
|
running version is served at `GET /healthz`, which is the one route outside
|
||||||
authentication.
|
authentication.
|
||||||
|
|
||||||
There is no database container. SQLite lives on a named volume, so a backup
|
There is no database container. SQLite lives in `./data`, bind-mounted into
|
||||||
is a file copy.
|
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
|
## Security
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -18,6 +18,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -36,7 +37,10 @@ var version = "dev"
|
|||||||
const (
|
const (
|
||||||
listenAddr = ":8080"
|
listenAddr = ":8080"
|
||||||
defaultTZ = "Europe/Helsinki"
|
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.
|
// failDelay throttles password guessing.
|
||||||
// ponytail: a fixed sleep is enough for a LAN-only app; swap in
|
// 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.
|
// openDB opens the SQLite file and brings its schema up to date.
|
||||||
func openDB(path string) (*sql.DB, error) {
|
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 +
|
dsn := "file:" + path +
|
||||||
"?_pragma=journal_mode(WAL)" +
|
"?_pragma=journal_mode(WAL)" +
|
||||||
"&_pragma=foreign_keys(ON)" +
|
"&_pragma=foreign_keys(ON)" +
|
||||||
|
|||||||
+6
-4
@@ -2,6 +2,11 @@ services:
|
|||||||
app:
|
app:
|
||||||
image: ${FOODSTER_REPO:?set FOODSTER_REPO in .env}:${FOODSTER_TAG:-latest}
|
image: ${FOODSTER_REPO:?set FOODSTER_REPO in .env}:${FOODSTER_TAG:-latest}
|
||||||
restart: unless-stopped
|
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:
|
ports:
|
||||||
- "${FOODSTER_PORT:-8080}:8080"
|
- "${FOODSTER_PORT:-8080}:8080"
|
||||||
environment:
|
environment:
|
||||||
@@ -9,7 +14,4 @@ services:
|
|||||||
FOODSTER_DB: /data/foodster.db
|
FOODSTER_DB: /data/foodster.db
|
||||||
TZ: ${TZ:-Europe/Helsinki}
|
TZ: ${TZ:-Europe/Helsinki}
|
||||||
volumes:
|
volumes:
|
||||||
- foodster-data:/data
|
- ./data:/data
|
||||||
|
|
||||||
volumes:
|
|
||||||
foodster-data:
|
|
||||||
|
|||||||
Reference in New Issue
Block a user