diff --git a/.env.example b/.env.example index 1b5879e..1fe4cb6 100644 --- a/.env.example +++ b/.env.example @@ -26,8 +26,7 @@ POSTGRES_PASSWORD=changeme-generate-a-long-random-password # DATABASE_URL=postgres://user:pass@host:5432/bookmarks?sslmode=require # Directory inside bookmark-api for immutable, content-addressed Cover bytes. -# Compose seeds and mounts its named volume at /covers, so keep this value -# /covers in this deployment. Standalone backend runs may choose another path. +# Compose builds the image and mounts its named volume at this path. COVER_DIR=/covers # --- Prod override (Traefik) only --- diff --git a/DEPLOY.md b/DEPLOY.md index 7852c65..f83a5e3 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -53,8 +53,8 @@ POSTGRES_PASSWORD= # not run; it then replaces the URL built from POSTGRES_PASSWORD above. # DATABASE_URL=postgres://user:pass@host:5432/bookmarks?sslmode=require -# Required path inside bookmark-api. Compose seeds and mounts the named -# cover-data volume at /covers, so keep this value /covers. +# Required path inside bookmark-api. Compose builds the image and mounts the +# named cover-data volume at this path. COVER_DIR=/covers # Required for the Traefik override. Both have no fallback — compose refuses diff --git a/README.md b/README.md index ce65da3..5a293a4 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ covers are stored, so the library renders in full with the browser switched off. | `OWNER_DISCORD_ID` | *(required)* | Discord user ID of the owner: seeded as the first Reader, owns every pre-registration bookmark, and is the only Reader who can revoke another's sessions. | | `ALLOWED_ORIGINS` | Asura + Demonic + Comix + Kagane origins | Comma-separated CORS allowlist. | | `DATABASE_URL` | *(required)* | Postgres connection URL, e.g. `postgres://bookmarks:…@postgres:5432/bookmarks?sslmode=disable`. Compose builds it from `POSTGRES_PASSWORD`. | -| `COVER_DIR` | *(required)* | Filesystem volume for immutable, content-addressed Cover bytes. Compose requires `/covers` and mounts `cover-data` there; standalone runs may choose another writable durable path. | +| `COVER_DIR` | *(required)* | Filesystem volume for immutable, content-addressed Cover bytes. Compose builds the image and mounts `cover-data` at this path; standalone runs may choose another writable durable path. | | `PORT` | `8080` | Plain HTTP; TLS terminated by the proxy. | | `BROWSER_WS_URL` | empty | CDP endpoint of the remote browser (`ws://:9222`), used to poll Kagane/Novelfull past their JS challenge and to fetch uncached Kagane covers. Must be an IP or `localhost` — Chrome's DevTools handler 500s any other Host header, MagicDNS names included. Unset disables both; stored covers still serve. | | `DISCORD_CLIENT_ID` | *(required)* | Discord application credentials for the browser sign-in (ADR-0002). | diff --git a/backend/Dockerfile b/backend/Dockerfile index d4beef9..fac830e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,6 +2,7 @@ # --- build stage: compile a static, CGO-free binary --- FROM golang:1.26-alpine AS build +ARG COVER_DIR=/covers WORKDIR /src # Dependencies first for layer caching (changes rarely). @@ -20,12 +21,13 @@ COPY internal/ ./internal/ RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/server . # Create the source directory; runtime COPY sets ownership for the named volume. -RUN mkdir -p /covers +RUN mkdir -p "$COVER_DIR" # --- runtime stage: distroless static, non-root --- FROM gcr.io/distroless/static:nonroot +ARG COVER_DIR=/covers WORKDIR / -COPY --from=build --chown=65532:65532 /covers /covers +COPY --from=build --chown=65532:65532 ${COVER_DIR} ${COVER_DIR} COPY --from=build /out/server /server EXPOSE 8080 USER nonroot:nonroot diff --git a/docker-compose.yml b/docker-compose.yml index b02a19d..36cebc4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,10 @@ services: bookmark-api: - build: ./backend + build: + context: ./backend + args: + COVER_DIR: ${COVER_DIR:?set COVER_DIR in .env} image: bookmarkmanager-backend:latest container_name: bookmark-api restart: unless-stopped @@ -26,8 +29,8 @@ services: # The bookmarks database. Host is the compose service name; the password # comes from .env so it is never committed. DATABASE_URL: ${DATABASE_URL:-postgres://bookmarks:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/bookmarks?sslmode=disable} - # Required path inside the API. The image seeds ownership at /covers and - # the named volume below mounts there; keep COVER_DIR=/covers in .env. + # Required path inside the API. The build seeds ownership at this path + # and the named volume below mounts there. COVER_DIR: ${COVER_DIR:?set COVER_DIR in .env} PORT: "8080" # Log timestamps only. Go's `log` stamps lines in local time, and this @@ -84,7 +87,7 @@ services: # is why a redeploy always ships the repo's script. - ./userscript:/userscript:ro # Content-addressed cover bytes survive API restarts and redeploys. - - cover-data:/covers + - cover-data:${COVER_DIR:?set COVER_DIR in .env} # Bound to loopback only: the proxy (or curl during smoke test) reaches it, # the public internet does not. ports: