diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9f4ef8e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +# ignore node_modules, build artifacts, git metadata +**/node_modules +frontend/dist +backend/node_modules +.git +.vscode +.idea +.env +npm-debug.log* +yarn-debug.log* +pnpm-debug.log* +Dockerfile* +docker-compose*.yml + +# Do NOT include runtime backend data in the build context +backend/data/ +backend/data/** + +# keep local dev artifacts out +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1eda71b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# Build frontend in a dedicated stage +FROM node:24-slim AS frontend-build +WORKDIR /app/frontend +COPY frontend/package*.json frontend/package-lock*.json ./ +RUN npm ci --silent +COPY frontend/ . +RUN npm run build + +# Final image: build backend, generate Prisma client, prune dev deps, and include frontend dist +FROM node:24-slim AS final +WORKDIR /app + +# install minimal OS deps +RUN apt-get update -y \ +&& apt-get install -y openssl + +# copy package manifests and install deps (leverage Docker cache) +COPY backend/package*.json backend/package-lock*.json ./backend/ +RUN cd backend && npm ci --silent + +# copy backend source (schema, migrations, source) +COPY backend ./backend + +# generate Prisma client against a harmless temp DB path (does not apply migrations) +RUN cd backend && DATABASE_URL="file:./data/sqlite.db" npx prisma generate --schema=./prisma/schema.prisma || true + +# remove devDependencies to slim image +RUN cd backend && npm prune --production --silent + +# install prisma CLI into final image so entrypoint can run migrate deploy at runtime +RUN cd backend && npm i --no-audit --no-fund prisma --silent || true + +# copy entrypoint (added below) and make executable +COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Copy built frontend into the expected location: /app/frontend/dist +COPY --from=frontend-build /app/frontend/dist ./frontend/dist + +# runtime environment (DATA_DIR used at runtime; DATABASE_URL points to it) +ENV NODE_ENV=production +ENV DATA_DIR=/data +ENV DATABASE_URL="file:${DATA_DIR}/sqlite.db" + +WORKDIR /app/backend +EXPOSE 3000 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["node", "server.js"] \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index da1d72b..ee6beca 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,10 @@ "dev": "nodemon server.js", "start": "node server.js", "prisma:generate": "prisma generate", - "prisma:migrate": "prisma migrate dev --name init" + "prisma:migrate-dev": "prisma migrate dev --name init", + "prisma:migrate-deploy": "prisma migrate deploy", + "prisma:studio": "prisma studio", + "prisma:deploy": "npx prisma migrate deploy && npx prisma generate" }, "dependencies": { "@prisma/client": "^6.19.0", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 6973c78..f32cdae 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -1,6 +1,6 @@ datasource db { provider = "sqlite" - url = "file:../data/sqlite.db" + url = env("DATABASE_URL") } generator client { diff --git a/backend/server.js b/backend/server.js index ab7f8b7..f93b62c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,8 +5,6 @@ const path = require('path') const cors = require('cors') const { PrismaClient } = require('@prisma/client') -const prisma = new PrismaClient() - // small, configurable logger used across the app const LOG_LEVEL = (process.env.LOG_LEVEL || 'info').toLowerCase() // 'error'|'info'|'debug' const levels = { error: 0, info: 1, debug: 2 } @@ -22,7 +20,7 @@ const log = { }, } -// store all runtime data under backend/data +// store runtime data under DATA_DIR env if provided, else backend/data const DATA_DIR = process.env.DATA_DIR ? path.resolve(process.env.DATA_DIR) : path.join(__dirname, 'data') @@ -33,6 +31,14 @@ const UPLOAD_DIR = path.join(DATA_DIR, 'uploads') if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }) if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true }) +// ensure DATABASE_URL points to the DB under DATA_DIR for Prisma at runtime +if (!process.env.DATABASE_URL) { + process.env.DATABASE_URL = `file:${path.join(DATA_DIR, 'sqlite.db')}` +} + +// instantiate Prisma client AFTER DATA_DIR / DATABASE_URL are ready +const prisma = new PrismaClient() + const upload = multer({ dest: UPLOAD_DIR }) // local storage for uploads const app = express() diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..5477176 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -eu + +DATA_DIR="${DATA_DIR:-/data}" +SCHEMA_PATH="./prisma/schema.prisma" + +# ensure data dir exists and is writable +mkdir -p "$DATA_DIR" +chmod 0775 "$DATA_DIR" || true + +# ensure DATABASE_URL is set to the runtime DATA_DIR +: "${DATABASE_URL:=file:${DATA_DIR}/sqlite.db}" +export DATABASE_URL + +# create sqlite file if missing +SQLITE_DB_PATH="${DATA_DIR}/sqlite.db" +if [ ! -f "$SQLITE_DB_PATH" ]; then + touch "$SQLITE_DB_PATH" + chmod 0664 "$SQLITE_DB_PATH" || true +fi + +# run non-interactive migrations if CLI available +if command -v npx >/dev/null 2>&1 && [ -f "$SCHEMA_PATH" ]; then + echo "Running prisma migrate deploy..." + npx prisma migrate deploy --schema="$SCHEMA_PATH" || echo "prisma migrate deploy failed or no migrations to apply" +fi + +# exec main process +exec "$@" \ No newline at end of file