release: repair the catalog 404 and stop the page jumping #3
+14
-7
@@ -1,22 +1,29 @@
|
|||||||
# Copy to .env and fill in. .env is gitignored — the real registry hostname
|
# Copy to .env and fill in. .env is gitignored — the real registry hostname
|
||||||
# must not end up in the repository.
|
# must not end up in the repository.
|
||||||
|
|
||||||
# Image coordinates. FOODSTER_REPO carries no tag.
|
# Image coordinates. REPO carries no tag.
|
||||||
FOODSTER_REPO=registry.example.com/you/foodster
|
REPO=registry.example.com/you/foodster
|
||||||
FOODSTER_TAG=latest
|
TAG=latest
|
||||||
|
|
||||||
# Shared household password. The app will not start without it.
|
# Shared household password. The app will not start without it.
|
||||||
FOODSTER_PASSWORD=changeme
|
PASSWORD=changeme
|
||||||
|
|
||||||
# Hostname Traefik routes to. Kept here rather than in compose.yaml so no
|
# Hostname Traefik routes to. Kept here rather than in compose.yaml so no
|
||||||
# infrastructure detail is committed.
|
# infrastructure detail is committed.
|
||||||
FOODSTER_HOST=foodster.example.com
|
HOST=foodster.example.com
|
||||||
|
|
||||||
|
# Anything other than prod is written into the browser tab title, so a dev
|
||||||
|
# instance open beside the real one can be told apart.
|
||||||
|
ENV=prod
|
||||||
|
|
||||||
# The database lives in ./data, bind-mounted into the container. These must
|
# The database lives in ./data, bind-mounted into the container. These must
|
||||||
# match whoever owns that directory on the host, or the container cannot
|
# match whoever owns that directory on the host, or the container cannot
|
||||||
# write to it. `id -u` and `id -g` will tell you.
|
# write to it. `id -u` and `id -g` will tell you.
|
||||||
FOODSTER_UID=1000
|
#
|
||||||
FOODSTER_GID=1000
|
# Named PUID/PGID because UID is read-only in bash and a plain UID here would
|
||||||
|
# be quietly replaced by the invoking shell's own.
|
||||||
|
PUID=1000
|
||||||
|
PGID=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
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ build: generate ## Build ./foodster
|
|||||||
-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 (database in ./data)
|
run: generate ## Run locally on :8080 (database in ./data)
|
||||||
FOODSTER_PASSWORD=$${FOODSTER_PASSWORD:-dev} go run $(PKG)
|
PASSWORD=$${PASSWORD:-dev} ENV=dev 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)
|
||||||
@@ -85,7 +85,7 @@ fix: ## Format Go and templ sources, tidy go.mod
|
|||||||
go mod tidy
|
go mod tidy
|
||||||
|
|
||||||
image: ## Build and tag an image as vYYYYMMDD-N. Creates a git tag.
|
image: ## Build and tag an image as vYYYYMMDD-N. Creates a git tag.
|
||||||
@test -n "$(FOODSTER_REPO)" || { echo "set FOODSTER_REPO in .env"; exit 1; }
|
@test -n "$(REPO)" || { echo "set REPO in .env"; exit 1; }
|
||||||
@# A release tag must point into main, or the tag records a commit that
|
@# A release tag must point into main, or the tag records a commit that
|
||||||
@# was never released.
|
@# was never released.
|
||||||
@branch=$$(git symbolic-ref --short HEAD); \
|
@branch=$$(git symbolic-ref --short HEAD); \
|
||||||
@@ -99,7 +99,7 @@ image: ## Build and tag an image as vYYYYMMDD-N. Creates a git tag.
|
|||||||
echo "==> $$tag"; \
|
echo "==> $$tag"; \
|
||||||
git tag "$$tag"; \
|
git tag "$$tag"; \
|
||||||
podman build --platform linux/amd64 --build-arg VERSION="$$tag" \
|
podman build --platform linux/amd64 --build-arg VERSION="$$tag" \
|
||||||
-t "$(FOODSTER_REPO):$$tag" -t "$(FOODSTER_REPO):latest" . ; \
|
-t "$(REPO):$$tag" -t "$(REPO):latest" . ; \
|
||||||
echo "$$tag" > $(TAGFILE)
|
echo "$$tag" > $(TAGFILE)
|
||||||
|
|
||||||
# Pushing reported success while uploading the previous release once, because
|
# Pushing reported success while uploading the previous release once, because
|
||||||
@@ -107,18 +107,18 @@ image: ## Build and tag an image as vYYYYMMDD-N. Creates a git tag.
|
|||||||
# registry what it actually serves for each tag and fail if it is not the
|
# registry what it actually serves for each tag and fail if it is not the
|
||||||
# image we just built.
|
# image we just built.
|
||||||
push: ## Push the newest tag and :latest, then verify the registry
|
push: ## Push the newest tag and :latest, then verify the registry
|
||||||
@test -n "$(FOODSTER_REPO)" || { echo "set FOODSTER_REPO in .env"; exit 1; }
|
@test -n "$(REPO)" || { echo "set REPO in .env"; exit 1; }
|
||||||
@test -f $(TAGFILE) || { echo "nothing built - run make image"; exit 1; }; \
|
@test -f $(TAGFILE) || { echo "nothing built - run make image"; exit 1; }; \
|
||||||
tag=$$(cat $(TAGFILE)); \
|
tag=$$(cat $(TAGFILE)); \
|
||||||
built=$$(podman image inspect "$(FOODSTER_REPO):$$tag" --format '{{.Id}}' 2>/dev/null) || \
|
built=$$(podman image inspect "$(REPO):$$tag" --format '{{.Id}}' 2>/dev/null) || \
|
||||||
{ echo "no local image tagged $$tag - run make image"; exit 1; }; \
|
{ echo "no local image tagged $$tag - run make image"; exit 1; }; \
|
||||||
podman push "$(FOODSTER_REPO):$$tag"; \
|
podman push "$(REPO):$$tag"; \
|
||||||
podman push "$(FOODSTER_REPO):latest"; \
|
podman push "$(REPO):latest"; \
|
||||||
echo "==> verifying $$tag"; \
|
echo "==> verifying $$tag"; \
|
||||||
for ref in "$$tag" latest; do \
|
for ref in "$$tag" latest; do \
|
||||||
podman pull -q "$(FOODSTER_REPO):$$ref" >/dev/null 2>&1 || \
|
podman pull -q "$(REPO):$$ref" >/dev/null 2>&1 || \
|
||||||
{ echo " FAIL $$ref is not in the registry"; exit 1; }; \
|
{ echo " FAIL $$ref is not in the registry"; exit 1; }; \
|
||||||
served=$$(podman image inspect "$(FOODSTER_REPO):$$ref" --format '{{.Id}}'); \
|
served=$$(podman image inspect "$(REPO):$$ref" --format '{{.Id}}'); \
|
||||||
if [ "$$served" != "$$built" ]; then \
|
if [ "$$served" != "$$built" ]; then \
|
||||||
echo " FAIL $$ref serves $$served"; \
|
echo " FAIL $$ref serves $$served"; \
|
||||||
echo " expected $$built"; \
|
echo " expected $$built"; \
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ build and no asset bundler.
|
|||||||
is imported because the runtime image carries no zoneinfo. All date logic
|
is imported because the runtime image carries no zoneinfo. All date logic
|
||||||
uses that location explicitly and never `time.Local`.
|
uses that location explicitly and never `time.Local`.
|
||||||
- **Auth**: HTTP Basic with one shared household password read from
|
- **Auth**: HTTP Basic with one shared household password read from
|
||||||
`FOODSTER_PASSWORD`; the username is ignored. Compared using
|
`PASSWORD`; the username is ignored. Compared using
|
||||||
`subtle.ConstantTimeCompare` over SHA-256 digests so neither the value nor
|
`subtle.ConstantTimeCompare` over SHA-256 digests so neither the value nor
|
||||||
its length leaks through timing. `/healthz` is the only route outside auth.
|
its length leaks through timing. `/healthz` is the only route outside auth.
|
||||||
- **Exposure**: the app is served on a public hostname behind Traefik, which
|
- **Exposure**: the app is served on a public hostname behind Traefik, which
|
||||||
@@ -395,21 +395,25 @@ on the server and run with Docker Compose.
|
|||||||
`/data/foodster.db`, bind-mounted from `./data` on the host rather than
|
`/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
|
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
|
through the container engine. Backup is `cp -r data`. Because the image
|
||||||
runs as UID 65534, compose sets `user:` from `FOODSTER_UID`/`FOODSTER_GID`
|
runs as UID 65534, compose sets `user:` from `PUID`/`PGID`
|
||||||
to match whoever owns that directory. `restart: unless-stopped`.
|
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.
|
Names carry no application prefix: the container namespaces them already.
|
||||||
- `FOODSTER_PASSWORD` — the shared password. Required; the app refuses to
|
- `REPO` and `TAG` — image coordinates.
|
||||||
start without it.
|
- `PASSWORD` — the shared password. Required; the app refuses to start
|
||||||
- `FOODSTER_DB` — database file path, default `./data/foodster.db`. The
|
without it.
|
||||||
directory is created on startup if missing.
|
- `DB` — database file path, default `./data/foodster.db`. The directory is
|
||||||
- `FOODSTER_UID` / `FOODSTER_GID` — host owner of `./data`.
|
created on startup if missing.
|
||||||
|
- `ENV` — anything but `prod` is prefixed to the browser tab title, so a
|
||||||
|
dev instance open beside the real one can be told apart.
|
||||||
|
- `PUID` / `PGID` — host owner of `./data`. Not `UID`, which is read-only
|
||||||
|
in bash and would be replaced by the invoking shell's own value.
|
||||||
- `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.
|
||||||
- **Routing**: Traefik on an external `traefik` network, matching on
|
- **Routing**: Traefik on an external `traefik` network, matching on
|
||||||
`FOODSTER_HOST` and terminating TLS. The container publishes no ports —
|
`HOST` and terminating TLS. The container publishes no ports —
|
||||||
doing so would put an unencrypted copy of the app on the host, bypassing
|
doing so would put an unencrypted copy of the app on the host, bypassing
|
||||||
the proxy. The hostname lives in `.env` rather than `compose.yaml`, so no
|
the proxy. The hostname lives in `.env` rather than `compose.yaml`, so no
|
||||||
infrastructure detail is committed.
|
infrastructure detail is committed.
|
||||||
|
|||||||
@@ -173,13 +173,19 @@ 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. |
|
| `PASSWORD` | *required* | Shared password. The app will not start without it. |
|
||||||
| `FOODSTER_DB` | `./data/foodster.db` | SQLite file path; the directory is created if missing. |
|
| `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. |
|
| `ENV` | `prod` | Anything else is prefixed to the tab title (`dev · Foodster`). |
|
||||||
|
| `ADDR` | `:8080` | Listen address. Only useful for a second local instance. |
|
||||||
|
| `PUID` / `PGID` | `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_TAG` | `latest` | Tag to run under compose. |
|
Names carry no prefix: the container gives them their own namespace already.
|
||||||
| `FOODSTER_PORT` | `8080` | Host port to publish. |
|
`PUID`/`PGID` are the exception — `UID` is read-only in bash, so a value set
|
||||||
|
in `.env` would be silently replaced by the invoking shell's own.
|
||||||
|
| `REPO` | *required to build* | Image repository, no tag. |
|
||||||
|
| `TAG` | `latest` | Tag to run under compose. |
|
||||||
|
| `HOST` | *required to run* | Hostname Traefik routes to. |
|
||||||
|
|
||||||
Set `TZ` in development too. Under UTC the date rolls over three hours late,
|
Set `TZ` in development too. Under UTC the date rolls over three hours late,
|
||||||
which is exactly when dinner gets logged.
|
which is exactly when dinner gets logged.
|
||||||
@@ -204,7 +210,7 @@ the container, so a backup is `cp -r data` and you can inspect the file with
|
|||||||
any sqlite client without going through the engine.
|
any sqlite client without going through the engine.
|
||||||
|
|
||||||
That directory must exist and be owned by the user compose runs as — `make up`
|
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
|
creates it, and `PUID`/`PGID` in `.env` tell the container who
|
||||||
that is. Get them from `id -u` and `id -g`.
|
that is. Get them from `id -u` and `id -g`.
|
||||||
|
|
||||||
If the app exits with `cannot open /data/foodster.db ... unable to open
|
If the app exits with `cannot open /data/foodster.db ... unable to open
|
||||||
@@ -213,7 +219,7 @@ bind-mount directory as root, and the container is not root:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
ls -ldn data # whose is it?
|
ls -ldn data # whose is it?
|
||||||
sudo chown -R 1000:1000 data # match FOODSTER_UID / FOODSTER_GID
|
sudo chown -R 1000:1000 data # match PUID / PGID
|
||||||
docker compose restart
|
docker compose restart
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -235,7 +241,7 @@ counting it would lock the household out for simply opening the app.
|
|||||||
address, meaning it arrived through the proxy. A client connecting directly
|
address, meaning it arrived through the proxy. A client connecting directly
|
||||||
could otherwise forge a new address per attempt and skip the limiter.
|
could otherwise forge a new address per attempt and skip the limiter.
|
||||||
|
|
||||||
**None of this replaces a strong `FOODSTER_PASSWORD`.** Rate limiting removes
|
**None of this replaces a strong `PASSWORD`.** Rate limiting removes
|
||||||
brute force as a practical route; it does not make a guessable password safe.
|
brute force as a practical route; it does not make a guessable password safe.
|
||||||
|
|
||||||
## Mockups
|
## Mockups
|
||||||
|
|||||||
+23
-6
@@ -35,6 +35,21 @@ var staticFS embed.FS
|
|||||||
// version is replaced at build time with the CalVer tag (see `make image`).
|
// version is replaced at build time with the CalVer tag (see `make image`).
|
||||||
var version = "dev"
|
var version = "dev"
|
||||||
|
|
||||||
|
// envTag marks the browser tab of anything that is not production, so a dev
|
||||||
|
// instance and the real one open side by side are told apart at a glance.
|
||||||
|
// Empty in production, which is the default.
|
||||||
|
var envTag string
|
||||||
|
|
||||||
|
func setEnvTag(value string) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" || strings.EqualFold(value, "prod") || strings.EqualFold(value, "production") {
|
||||||
|
envTag = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Whatever it says, so ENV=staging labels itself too.
|
||||||
|
envTag = strings.ToLower(value)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
listenAddr = ":8080"
|
listenAddr = ":8080"
|
||||||
defaultTZ = "Europe/Helsinki"
|
defaultTZ = "Europe/Helsinki"
|
||||||
@@ -55,7 +70,7 @@ func run() error {
|
|||||||
"import a JSON dish bundle (PRD §7.3 shape) and exit")
|
"import a JSON dish bundle (PRD §7.3 shape) and exit")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
db, err := openDB(cmp.Or(os.Getenv("FOODSTER_DB"), defaultDB))
|
db, err := openDB(cmp.Or(os.Getenv("DB"), defaultDB))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -66,9 +81,9 @@ func run() error {
|
|||||||
return runImport(db, *importPath)
|
return runImport(db, *importPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
password := os.Getenv("FOODSTER_PASSWORD")
|
password := os.Getenv("PASSWORD")
|
||||||
if password == "" {
|
if password == "" {
|
||||||
return errors.New("FOODSTER_PASSWORD is not set")
|
return errors.New("PASSWORD is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fail rather than fall back to UTC: a silently wrong zone shifts logged
|
// Fail rather than fall back to UTC: a silently wrong zone shifts logged
|
||||||
@@ -79,9 +94,11 @@ func run() error {
|
|||||||
return fmt.Errorf("TZ: %w", err)
|
return fmt.Errorf("TZ: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The container always publishes :8080; FOODSTER_ADDR exists so tests and
|
setEnvTag(os.Getenv("ENV"))
|
||||||
// a second local instance can pick another port.
|
|
||||||
addr := cmp.Or(os.Getenv("FOODSTER_ADDR"), listenAddr)
|
// The container always publishes :8080; ADDR exists so tests and a second
|
||||||
|
// local instance can pick another port.
|
||||||
|
addr := cmp.Or(os.Getenv("ADDR"), listenAddr)
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
|
|||||||
@@ -105,6 +105,29 @@ func TestReadSignals(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnvTagMarksNonProduction(t *testing.T) {
|
||||||
|
t.Cleanup(func() { envTag = "" })
|
||||||
|
|
||||||
|
cases := []struct{ env, want string }{
|
||||||
|
// Production is the default and must stay unmarked: the tag exists to
|
||||||
|
// pick the dev tab out of two identical ones.
|
||||||
|
{"", "Foodster"},
|
||||||
|
{"prod", "Foodster"},
|
||||||
|
{"PRODUCTION", "Foodster"},
|
||||||
|
{" ", "Foodster"},
|
||||||
|
{"dev", "dev · Foodster"},
|
||||||
|
{"DEV", "dev · Foodster"},
|
||||||
|
{"staging", "staging · Foodster"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
setEnvTag(c.env)
|
||||||
|
if got := pageTitle("Foodster"); got != c.want {
|
||||||
|
t.Errorf("ENV=%q: title = %q, want %q", c.env, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMigrateCreatesSchema(t *testing.T) {
|
func TestMigrateCreatesSchema(t *testing.T) {
|
||||||
db, err := openDB(t.TempDir() + "/test.db")
|
db, err := openDB(t.TempDir() + "/test.db")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -108,6 +108,16 @@ func categoryLabels(d Dish) string {
|
|||||||
return strings.Join(names, ", ")
|
return strings.Join(names, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pageTitle prefixes the tab title on any instance that is not production.
|
||||||
|
// The tab is the only place a browser shows which of two identical apps you
|
||||||
|
// are looking at.
|
||||||
|
func pageTitle(title string) string {
|
||||||
|
if envTag == "" {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
return envTag + " · " + title
|
||||||
|
}
|
||||||
|
|
||||||
// countFI renders "1 pääruoka" but "16 pääruokaa": Finnish takes the partitive
|
// countFI renders "1 pääruoka" but "16 pääruokaa": Finnish takes the partitive
|
||||||
// after every number except one.
|
// after every number except one.
|
||||||
func countFI(n int, one, many string) string {
|
func countFI(n int, one, many string) string {
|
||||||
@@ -130,7 +140,7 @@ templ page(title, current string) {
|
|||||||
// header rather than butting against it.
|
// header rather than butting against it.
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#FFFFFF"/>
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#FFFFFF"/>
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1E22"/>
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1E22"/>
|
||||||
<title>{ title }</title>
|
<title>{ pageTitle(title) }</title>
|
||||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml"/>
|
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml"/>
|
||||||
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png"/>
|
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png"/>
|
||||||
<!-- use-credentials: the manifest is fetched behind Basic auth and
|
<!-- use-credentials: the manifest is fetched behind Basic auth and
|
||||||
|
|||||||
+13
-9
@@ -1,19 +1,23 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: ${FOODSTER_REPO:?set FOODSTER_REPO in .env}:${FOODSTER_TAG:-latest}
|
image: ${REPO:?set REPO in .env}:${TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# The database is a bind mount, not a named volume: it sits in ./data on
|
# A bind mount rather than a named volume: the database sits in ./data on
|
||||||
# the host where it can be listed, copied and opened with any sqlite
|
# the host, where it can be listed, copied and backed up without going
|
||||||
# client. The image runs as UID 65534, so the container has to be told
|
# through the container engine. The image runs as UID 65534, so the
|
||||||
# which host user owns that directory.
|
# container has to be told which host user owns that directory.
|
||||||
user: "${FOODSTER_UID:-1000}:${FOODSTER_GID:-1000}"
|
#
|
||||||
|
# PUID/PGID rather than UID/GID: UID is a read-only variable in bash, so a
|
||||||
|
# value set here would be silently replaced by the invoking shell's own.
|
||||||
|
user: "${PUID:-1000}:${PGID:-1000}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
FOODSTER_PASSWORD: ${FOODSTER_PASSWORD:?set FOODSTER_PASSWORD in .env}
|
PASSWORD: ${PASSWORD:?set PASSWORD in .env}
|
||||||
FOODSTER_DB: /data/foodster.db
|
DB: /data/foodster.db
|
||||||
|
ENV: ${ENV:-prod}
|
||||||
TZ: ${TZ:-Europe/Helsinki}
|
TZ: ${TZ:-Europe/Helsinki}
|
||||||
|
|
||||||
# No published ports: Traefik reaches the container over the shared
|
# No published ports: Traefik reaches the container over the shared
|
||||||
@@ -22,7 +26,7 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.foodster.entrypoints=websecure
|
- traefik.http.routers.foodster.entrypoints=websecure
|
||||||
- traefik.http.routers.foodster.rule=Host(`${FOODSTER_HOST:?set FOODSTER_HOST in .env}`)
|
- traefik.http.routers.foodster.rule=Host(`${HOST:?set HOST in .env}`)
|
||||||
- traefik.http.routers.foodster.tls=true
|
- traefik.http.routers.foodster.tls=true
|
||||||
- traefik.http.services.foodster.loadbalancer.server.port=8080
|
- traefik.http.services.foodster.loadbalancer.server.port=8080
|
||||||
- traefik.docker.network=traefik
|
- traefik.docker.network=traefik
|
||||||
|
|||||||
+3
-1
@@ -16,7 +16,7 @@ trap 'kill ${srv:-0} 2>/dev/null || true; rm -rf "$tmp"' EXIT
|
|||||||
|
|
||||||
go build -o "$tmp/foodster" ./cmd/foodster
|
go build -o "$tmp/foodster" ./cmd/foodster
|
||||||
|
|
||||||
FOODSTER_PASSWORD="$pass" FOODSTER_DB="$tmp/smoke.db" FOODSTER_ADDR="$addr" \
|
PASSWORD="$pass" DB="$tmp/smoke.db" ADDR="$addr" \
|
||||||
"$tmp/foodster" >"$tmp/server.log" 2>&1 &
|
"$tmp/foodster" >"$tmp/server.log" 2>&1 &
|
||||||
srv=$!
|
srv=$!
|
||||||
|
|
||||||
@@ -72,6 +72,8 @@ check "theme script is served" \
|
|||||||
|
|
||||||
home=$(curl -s -u ":$pass" "http://$addr/")
|
home=$(curl -s -u ":$pass" "http://$addr/")
|
||||||
check "the header carries the brand" "$home" "Foodster"
|
check "the header carries the brand" "$home" "Foodster"
|
||||||
|
# ENV is unset here, so this instance is production and unmarked.
|
||||||
|
check "production tabs are not tagged" "$home" "<title>Foodster</title>"
|
||||||
check "dark is the default without JavaScript" "$home" '<html lang="fi" data-theme="dark">'
|
check "dark is the default without JavaScript" "$home" '<html lang="fi" data-theme="dark">'
|
||||||
check "the theme toggle is present" "$home" "data-theme-toggle"
|
check "the theme toggle is present" "$home" "data-theme-toggle"
|
||||||
check "both theme icons ship so CSS can pick one" "$home" 'class="i-moon"'
|
check "both theme icons ship so CSS can pick one" "$home" 'class="i-moon"'
|
||||||
|
|||||||
Reference in New Issue
Block a user