From ed6e06143686fbb363e5fae494baaf0d7eee03bc Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Tue, 3 Mar 2026 14:26:08 -0500 Subject: [PATCH] Introduce DATA_DIR env var for consistent data path configuration Replace separate DATABASE_URL and IMAGE_CACHE_DIR env vars with a single DATA_DIR root (default: ./data, Docker: /data). DATABASE_URL and CACHE_DIR are derived from it but can still be overridden individually. Also: - Pin pnpm to @10 for reproducible Docker builds - Add --link to COPY instructions for better BuildKit cache reuse - Add syntax directive for BuildKit features - Simplify Dockerfile mkdir to just /data (ensureImageDirs handles subdirs) - Remove redundant DATABASE_URL from docker-compose.yml Co-Authored-By: Claude Opus 4.6 --- .env.example | 12 +++++------- CLAUDE.md | 4 ++-- Dockerfile | 14 ++++++++------ docker-compose.yml | 3 +-- drizzle.config.ts | 5 ++++- lib/db/client.ts | 5 ++++- lib/services/image-cache.ts | 5 ++++- 7 files changed, 28 insertions(+), 20 deletions(-) diff --git a/.env.example b/.env.example index fcd893f..118e9aa 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,10 @@ -# ─── Database ─────────────────────────────────────────────────────────── -# SQLite database URL (Docker: file:/data/sqlite.db, local dev: file:sqlite.db) -# DATABASE_URL=file:/data/sqlite.db +# ─── Data Directory ────────────────────────────────────────────────────── +# Root directory for SQLite database and image cache (default: ./data, Docker: /data) +# DATA_DIR=./data # ─── TMDB (required) ─────────────────────────────────────────────────────── # API Read Access Token — get one at https://www.themoviedb.org/settings/api -TMDB_API_READ_ACCESS_TOKEN=your_tmdb_api_read_access_token_here +TMDB_API_READ_ACCESS_TOKEN= # Optional: override TMDB base URLs (advanced) # TMDB_API_BASE_URL=https://api.themoviedb.org/3 @@ -13,7 +13,7 @@ TMDB_API_READ_ACCESS_TOKEN=your_tmdb_api_read_access_token_here # ─── Auth (required) ─────────────────────────────────────────────────────── # Random secret for session encryption (min 32 chars) # Generate one with `npx @better-auth/cli secret` or `openssl rand -base64 32` -BETTER_AUTH_SECRET=your_secret_here +BETTER_AUTH_SECRET= # Public URL of your instance, especially important if reverse proxy is used BETTER_AUTH_URL=http://localhost:3000 @@ -28,7 +28,5 @@ BETTER_AUTH_URL=http://localhost:3000 # DISABLE_PASSWORD_LOGIN=false # Set to "true" to hide email/password form when OIDC is configured # ─── Image Caching ───────────────────────────────────────────────────── -# Downloads TMDB images to local disk for faster serving (default: /data/images) -# IMAGE_CACHE_DIR=/data/images # Set IMAGE_CACHE_ENABLED to "false" to use TMDB CDN directly (default: enabled) # IMAGE_CACHE_ENABLED=true diff --git a/CLAUDE.md b/CLAUDE.md index 0ddf511..9233a4d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,7 +80,7 @@ All app tables use UUID text primary keys generated via the `uuid` package. Bett Only paths are stored in DB. Image URLs are resolved **server-side only** — API routes and server components call `tmdbImageUrl()` from `lib/tmdb/image.ts` before sending data to clients. Client components never import `tmdbImageUrl`; they receive ready-to-use URLs. -When `IMAGE_CACHE_ENABLED` is set (default), images are downloaded to local disk (`IMAGE_CACHE_DIR`, defaults to `/data/images`) and served via `app/api/images/[...path]/route.ts`. Categories: `posters` (w500), `backdrops` (w1280), `stills` (w1280), `logos` (w92). When disabled, `tmdbImageUrl()` returns direct TMDB CDN URLs using `TMDB_IMAGE_BASE_URL` (defaults to `https://image.tmdb.org/t/p`). +When `IMAGE_CACHE_ENABLED` is set (default), images are downloaded to local disk (`CACHE_DIR`, derived from `DATA_DIR/images`) and served via `app/api/images/[...path]/route.ts`. Categories: `posters` (w500), `backdrops` (w1280), `stills` (w1280), `logos` (w92). When disabled, `tmdbImageUrl()` returns direct TMDB CDN URLs using `TMDB_IMAGE_BASE_URL` (defaults to `https://image.tmdb.org/t/p`). Core files: `lib/services/image-cache.ts` (caching logic), `lib/tmdb/image.ts` (URL construction), `app/api/images/[...path]/route.ts` (proxy route). @@ -90,7 +90,7 @@ Registered in `lib/jobs/registry.ts`, started via Next.js instrumentation hook ( ### Environment variables -See `.env.example`: `DATABASE_URL`, `TMDB_API_READ_ACCESS_TOKEN`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`. +See `.env.example`: `DATA_DIR` (root for DB + cache, default `./data`), `TMDB_API_READ_ACCESS_TOKEN`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`. `DATABASE_URL` and `CACHE_DIR` are derived from `DATA_DIR` but can be overridden individually. ## Browser Automation diff --git a/Dockerfile b/Dockerfile index 661ba44..8354beb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ +# syntax=docker/dockerfile:1 FROM node:24-alpine AS base -RUN corepack enable && corepack prepare pnpm@latest --activate +RUN corepack enable && corepack prepare pnpm@10 --activate # --- Dependencies --- FROM base AS deps @@ -25,16 +26,17 @@ WORKDIR /app ENV NODE_ENV=production ENV HOSTNAME=0.0.0.0 ENV PORT=3000 +ENV DATA_DIR=/data RUN addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 nextjs \ - && mkdir -p /data/images/posters /data/images/backdrops /data/images/stills /data/images/logos \ + && mkdir -p /data \ && chown -R nextjs:nodejs /data -COPY --from=builder --chown=nextjs:nodejs /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle +COPY --from=builder --link --chown=nextjs:nodejs /app/public ./public +COPY --from=builder --link --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --link --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --link --chown=nextjs:nodejs /app/drizzle ./drizzle USER nextjs EXPOSE 3000 diff --git a/docker-compose.yml b/docker-compose.yml index 4d9b5d4..69e88c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: sofa: - image: ghcr.io/jakejarvis/sofa:latest + image: ghcr.io/jakejarvis/sofa:edge container_name: sofa restart: unless-stopped ports: @@ -8,7 +8,6 @@ services: volumes: - sofa-data:/data environment: - - DATABASE_URL=file:/data/sqlite.db - TMDB_API_READ_ACCESS_TOKEN=${TMDB_API_READ_ACCESS_TOKEN} - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} - BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:3000} diff --git a/drizzle.config.ts b/drizzle.config.ts index 8bf11a8..cdd35b8 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,10 +1,13 @@ import { defineConfig } from "drizzle-kit"; +const DATA_DIR = process.env.DATA_DIR || "./data"; +const DATABASE_URL = process.env.DATABASE_URL || `file:${DATA_DIR}/sqlite.db`; + export default defineConfig({ dialect: "turso", schema: "./lib/db/schema.ts", out: "./drizzle", dbCredentials: { - url: process.env.DATABASE_URL ?? "file:./data/sqlite.db", + url: DATABASE_URL, }, }); diff --git a/lib/db/client.ts b/lib/db/client.ts index dc563ce..ad196ca 100644 --- a/lib/db/client.ts +++ b/lib/db/client.ts @@ -7,10 +7,13 @@ const globalForDb = globalThis as unknown as { _client: ReturnType | undefined; }; +const DATA_DIR = process.env.DATA_DIR || "./data"; +const DATABASE_URL = process.env.DATABASE_URL || `file:${DATA_DIR}/sqlite.db`; + function getClient() { if (!globalForDb._client) { globalForDb._client = createClient({ - url: process.env.DATABASE_URL ?? "file:./data/sqlite.db", + url: DATABASE_URL, }); globalForDb._client.execute("PRAGMA journal_mode = WAL"); globalForDb._client.execute("PRAGMA foreign_keys = ON"); diff --git a/lib/services/image-cache.ts b/lib/services/image-cache.ts index 86de092..99a1fd3 100644 --- a/lib/services/image-cache.ts +++ b/lib/services/image-cache.ts @@ -17,7 +17,10 @@ const CATEGORY_SIZES: Record = { const IMAGE_BASE_URL = process.env.TMDB_IMAGE_BASE_URL || "https://image.tmdb.org/t/p"; -const CACHE_DIR = process.env.IMAGE_CACHE_DIR || "/data/images"; +const DATA_DIR = process.env.DATA_DIR || "./data"; +const CACHE_DIR = process.env.CACHE_DIR + ? path.join(process.env.CACHE_DIR, "images") + : path.join(DATA_DIR, "images"); export function imageCacheEnabled(): boolean { return process.env.IMAGE_CACHE_ENABLED !== "false";