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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-13 12:53:52 -07:00
commit 445a282ef1
10 changed files with 247 additions and 0 deletions

9
.env.example Normal file
View File

@ -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

32
.gitignore vendored Normal file
View File

@ -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

38
Dockerfile Normal file
View File

@ -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"]

64
docker-compose.yml Normal file
View File

@ -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:

6
next.config.mjs Normal file
View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
export default nextConfig;

34
package.json Normal file
View File

@ -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"
}
}

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

13
src/app/globals.css Normal file
View File

@ -0,0 +1,13 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
body {
color: var(--foreground);
background: var(--background);
}

17
tailwind.config.ts Normal file
View File

@ -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;

26
tsconfig.json Normal file
View File

@ -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"]
}