commit 445a282ef12157e4fc5e680d11373e80981b569e Author: Jeff Emmett Date: Fri Feb 13 12:53:52 2026 -0700 Initial scaffold: Next.js 14 + Prisma + Docker Project structure matching rTrips-online pattern: - Multi-stage Dockerfile with nextjs user - Phase 3 hardened docker-compose (cap_drop ALL, read_only) - Traefik labels for rnotes.online routing - PostgreSQL 16-alpine on internal network Co-Authored-By: Claude Opus 4.6 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0204fd7 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Database +DB_PASSWORD=changeme + +# rSpace integration +NEXT_PUBLIC_RSPACE_URL=https://rspace.online +RSPACE_INTERNAL_URL=http://rspace-online:3000 + +# EncryptID +NEXT_PUBLIC_ENCRYPTID_SERVER_URL=https://encryptid.jeffemmett.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8762a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env*.local + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1a51686 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM node:20-alpine AS base + +# Dependencies stage +FROM base AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +COPY prisma ./prisma/ +RUN npm ci || npm install + +# Build stage +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npx prisma generate +RUN npm run build + +# Production stage +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /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 /app/prisma ./prisma +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma + +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..15e629a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +services: + rnotes: + build: . + container_name: rnotes-online + restart: unless-stopped + environment: + - DATABASE_URL=postgresql://rnotes:${DB_PASSWORD}@rnotes-postgres:5432/rnotes + - NEXT_PUBLIC_RSPACE_URL=${NEXT_PUBLIC_RSPACE_URL:-https://rspace.online} + - RSPACE_INTERNAL_URL=${RSPACE_INTERNAL_URL:-http://rspace-online:3000} + - NEXT_PUBLIC_ENCRYPTID_SERVER_URL=${NEXT_PUBLIC_ENCRYPTID_SERVER_URL:-https://encryptid.jeffemmett.com} + labels: + - "traefik.enable=true" + - "traefik.http.routers.rnotes.rule=Host(`rnotes.online`) || Host(`www.rnotes.online`)" + - "traefik.http.services.rnotes.loadbalancer.server.port=3000" + networks: + - traefik-public + - rnotes-internal + depends_on: + rnotes-postgres: + condition: service_healthy + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + read_only: true + tmpfs: + - /tmp + - /home/nextjs/.npm + + rnotes-postgres: + image: postgres:16-alpine + container_name: rnotes-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=rnotes + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=rnotes + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - rnotes-internal + healthcheck: + test: ["CMD-SHELL", "pg_isready -U rnotes -d rnotes"] + interval: 5s + timeout: 5s + retries: 5 + cap_drop: + - ALL + cap_add: + - DAC_OVERRIDE + - FOWNER + - SETGID + - SETUID + security_opt: + - no-new-privileges:true + +networks: + traefik-public: + external: true + rnotes-internal: + internal: true + +volumes: + postgres_data: diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..e25a6a2 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..3f3899d --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "rnotes-online", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "db:push": "npx prisma db push", + "db:migrate": "npx prisma migrate dev", + "db:studio": "npx prisma studio" + }, + "dependencies": { + "@prisma/client": "^6.19.2", + "nanoid": "^5.0.9", + "next": "14.2.35", + "react": "^18", + "react-dom": "^18", + "zustand": "^5.0.11", + "marked": "^15.0.0", + "dompurify": "^3.2.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/dompurify": "^3", + "postcss": "^8", + "prisma": "^6.19.2", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..d477be5 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #0a0a0a; + --foreground: #ededed; +} + +body { + color: var(--foreground); + background: var(--background); +} diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..347d597 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,17 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./src/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + background: "var(--background)", + foreground: "var(--foreground)", + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}