Compare commits

..

2 Commits

Author SHA1 Message Date
user
93968b6f10 fix: unify DATA_DIR default to /var/lib/webhooker for all environments
All checks were successful
check / check (push) Successful in 1m4s
Remove devDataDir() XDG-based logic. Both dev and prod now default
DATA_DIR to /var/lib/webhooker. Update Dockerfile and README to match.
2026-03-17 04:42:35 -07:00
clawbot
89af414037 fix: use absolute path for dev DATA_DIR default, clarify env docs
Change the dev-mode DATA_DIR default from the relative path ./data to
$XDG_DATA_HOME/webhooker (falling back to $HOME/.local/share/webhooker).
This ensures the application's data directory does not depend on the
working directory.

Add a table to the README that clearly documents what WEBHOOKER_ENVIRONMENT
actually controls: DATA_DIR default, CORS policy, and session cookie
Secure flag.

Add tests for devDataDir() and verify the dev default is always absolute.
2026-03-17 04:41:09 -07:00
4 changed files with 46 additions and 86 deletions

View File

@@ -57,10 +57,10 @@ WORKDIR /app
COPY --from=builder /build/bin/webhooker .
# Create data directory for all SQLite databases (main app DB +
# per-webhook event DBs). DATA_DIR defaults to /data in production.
RUN mkdir -p /data
# per-webhook event DBs). DATA_DIR defaults to /var/lib/webhooker.
RUN mkdir -p /var/lib/webhooker
RUN chown -R webhooker:webhooker /app /data
RUN chown -R webhooker:webhooker /app /var/lib/webhooker
USER webhooker

View File

@@ -59,7 +59,6 @@ or `prod` (default: `dev`). The setting controls several behaviors:
| Behavior | `dev` | `prod` |
| --------------------- | -------------------------------- | ------------------------------- |
| Default `DATA_DIR` | `$XDG_DATA_HOME/webhooker` (or `$HOME/.local/share/webhooker`) | `/data` |
| CORS | Allows any origin (`*`) | Disabled (no-op) |
| Session cookie Secure | `false` (works over plain HTTP) | `true` (requires HTTPS) |
@@ -71,7 +70,7 @@ TTY detection, and security headers are always applied.
| ----------------------- | ----------------------------------- | -------- |
| `WEBHOOKER_ENVIRONMENT` | `dev` or `prod` | `dev` |
| `PORT` | HTTP listen port | `8080` |
| `DATA_DIR` | Directory for all SQLite databases | `$XDG_DATA_HOME/webhooker` (dev) / `/data` (prod) |
| `DATA_DIR` | Directory for all SQLite databases | `/var/lib/webhooker` |
| `DEBUG` | Enable debug logging | `false` |
| `METRICS_USERNAME` | Basic auth username for `/metrics` | `""` |
| `METRICS_PASSWORD` | Basic auth password for `/metrics` | `""` |
@@ -90,16 +89,16 @@ is only displayed once.
```bash
docker run -d \
-p 8080:8080 \
-v /path/to/data:/data \
-v /path/to/data:/var/lib/webhooker \
-e WEBHOOKER_ENVIRONMENT=prod \
webhooker:latest
```
The container runs as a non-root user (`webhooker`, UID 1000), exposes
port 8080, and includes a health check against
`/.well-known/healthcheck`. The `/data` volume holds all SQLite
databases: the main application database (`webhooker.db`) and the
per-webhook event databases (`events-{uuid}.db`). Mount this as a
`/.well-known/healthcheck`. The `/var/lib/webhooker` volume holds all
SQLite databases: the main application database (`webhooker.db`) and
the per-webhook event databases (`events-{uuid}.db`). Mount this as a
persistent volume to preserve data across container restarts.
## Rationale
@@ -845,8 +844,8 @@ The Dockerfile uses a multi-stage build:
golangci-lint, downloads dependencies, copies source, runs `make
check` (format verification, linting, tests, compilation).
2. **Runtime stage** (`alpine:3.21`) — copies the binary, creates the
`/data` directory for all SQLite databases, runs as non-root user,
exposes port 8080, includes a health check.
`/var/lib/webhooker` directory for all SQLite databases, runs as
non-root user, exposes port 8080, includes a health check.
The builder uses Debian rather than Alpine because GORM's SQLite
dialect pulls in CGO-dependent headers at compile time. The runtime

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"log/slog"
"os"
"path/filepath"
"strconv"
"strings"
@@ -80,23 +79,6 @@ func envInt(key string, defaultValue int) int {
return defaultValue
}
// devDataDir returns the default data directory for the dev
// environment. It uses $XDG_DATA_HOME/webhooker if set, otherwise
// falls back to $HOME/.local/share/webhooker. The result is always
// an absolute path so the application's behavior does not depend on
// the working directory.
func devDataDir() string {
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
return filepath.Join(xdg, "webhooker")
}
home, err := os.UserHomeDir()
if err != nil {
// Last resort: use /tmp so we still have an absolute path.
return filepath.Join(os.TempDir(), "webhooker")
}
return filepath.Join(home, ".local", "share", "webhooker")
}
// nolint:revive // lc parameter is required by fx even if unused
func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
log := params.Logger.Get()
@@ -127,16 +109,11 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
params: &params,
}
// Set default DataDir based on environment. All SQLite databases
// (main application DB and per-webhook event DBs) live here.
// Both defaults are absolute paths to avoid dependence on the
// working directory.
// Set default DataDir. All SQLite databases (main application DB
// and per-webhook event DBs) live here. The same default is used
// regardless of environment; override with DATA_DIR if needed.
if s.DataDir == "" {
if s.IsProd() {
s.DataDir = "/data"
} else {
s.DataDir = devDataDir()
}
s.DataDir = "/var/lib/webhooker"
}
if s.Debug {

View File

@@ -2,7 +2,6 @@ package config
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@@ -106,37 +105,21 @@ func TestEnvironmentConfig(t *testing.T) {
}
}
func TestDevDataDir(t *testing.T) {
t.Run("uses XDG_DATA_HOME when set", func(t *testing.T) {
os.Setenv("XDG_DATA_HOME", "/custom/data")
defer os.Unsetenv("XDG_DATA_HOME")
got := devDataDir()
assert.Equal(t, "/custom/data/webhooker", got)
})
t.Run("falls back to HOME/.local/share/webhooker", func(t *testing.T) {
os.Unsetenv("XDG_DATA_HOME")
home, err := os.UserHomeDir()
require.NoError(t, err)
got := devDataDir()
assert.Equal(t, filepath.Join(home, ".local", "share", "webhooker"), got)
})
t.Run("result is always absolute", func(t *testing.T) {
os.Unsetenv("XDG_DATA_HOME")
got := devDataDir()
assert.True(t, filepath.IsAbs(got), "devDataDir() returned relative path: %s", got)
})
}
func TestDevDefaultDataDirIsAbsolute(t *testing.T) {
// Verify that when WEBHOOKER_ENVIRONMENT=dev and DATA_DIR is unset,
// the resulting DataDir is an absolute path.
func TestDefaultDataDir(t *testing.T) {
// Verify that when DATA_DIR is unset, the default is /var/lib/webhooker
// regardless of the environment setting.
for _, env := range []string{"", "dev", "prod"} {
name := env
if name == "" {
name = "unset"
}
t.Run("env="+name, func(t *testing.T) {
if env != "" {
os.Setenv("WEBHOOKER_ENVIRONMENT", env)
defer os.Unsetenv("WEBHOOKER_ENVIRONMENT")
} else {
os.Unsetenv("WEBHOOKER_ENVIRONMENT")
}
os.Unsetenv("DATA_DIR")
var cfg *Config
@@ -153,6 +136,7 @@ func TestDevDefaultDataDirIsAbsolute(t *testing.T) {
app.RequireStart()
defer app.RequireStop()
assert.True(t, filepath.IsAbs(cfg.DataDir),
"dev default DataDir should be absolute, got: %s", cfg.DataDir)
assert.Equal(t, "/var/lib/webhooker", cfg.DataDir)
})
}
}