diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..66702e6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.next +.git +.env* +!.env.example +*.log +*.tsbuildinfo +next-env.d.ts +.DS_Store +storage +tests +docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..317384f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM node:24-alpine AS base +RUN corepack enable && corepack prepare pnpm@latest --activate + +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN pnpm build + +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs + +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 --chown=nextjs:nodejs /app/docs/style-guide.md ./docs/style-guide.md +COPY --from=builder --chown=nextjs:nodejs /app/scripts/migrate.mjs ./scripts/migrate.mjs +COPY --chown=nextjs:nodejs entrypoint.sh ./entrypoint.sh +RUN chmod +x entrypoint.sh + +RUN mkdir -p storage && chown -R nextjs:nodejs storage + +USER nextjs +EXPOSE 4700 +ENV PORT=4700 +ENV HOSTNAME="0.0.0.0" +ENV STORAGE_DIR=/app/storage + +CMD ["./entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..70fdc25 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +if [ "$SKIP_MIGRATIONS" != "true" ]; then + node scripts/migrate.mjs +fi + +exec node server.js diff --git a/next.config.ts b/next.config.ts index c47a734..5954a8c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,11 @@ import type { NextConfig } from 'next' import createNextIntlPlugin from 'next-intl/plugin' const config: NextConfig = { + output: 'standalone', + outputFileTracingIncludes: { + '/api/v1/style-guide': ['./docs/style-guide.md'], + '/': ['./node_modules/drizzle-orm/**'], + }, typedRoutes: true, experimental: { useTypeScriptCli: true, diff --git a/scripts/migrate.mjs b/scripts/migrate.mjs new file mode 100644 index 0000000..ce496ff --- /dev/null +++ b/scripts/migrate.mjs @@ -0,0 +1,18 @@ +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { drizzle } from 'drizzle-orm/node-postgres' +import pg from 'pg' + +const url = process.env.DATABASE_URL + +if (!url) { + console.error('[migrate] DATABASE_URL is required') + process.exit(1) +} + +const pool = new pg.Pool({ connectionString: url, max: 1 }) +const db = drizzle(pool) + +console.log('[migrate] running logbuch migrations') +await migrate(db, { migrationsFolder: './drizzle' }) +console.log('[migrate] done') +await pool.end() diff --git a/src/data/db.ts b/src/data/db.ts index 6877657..eb87e7e 100644 --- a/src/data/db.ts +++ b/src/data/db.ts @@ -2,13 +2,41 @@ import { drizzle } from 'drizzle-orm/node-postgres' import { Pool } from 'pg' import * as schema from './schema' -const isTest = process.env.VITEST === 'true' || process.env.NODE_ENV === 'test' -const url = isTest ? process.env.DATABASE_URL_TEST : process.env.DATABASE_URL +type Client = ReturnType> -if (!url) { - throw new Error(isTest ? 'DATABASE_URL_TEST is not set' : 'DATABASE_URL is not set') +let connection: { pool: Pool, db: Client } | undefined + +function connectionUrl(): string { + const isTest = process.env.VITEST === 'true' || process.env.NODE_ENV === 'test' + const url = isTest ? process.env.DATABASE_URL_TEST : process.env.DATABASE_URL + + if (!url) { + throw new Error(isTest ? 'DATABASE_URL_TEST is not set' : 'DATABASE_URL is not set') + } + + return url } -export const pool = new Pool({ connectionString: url }) -export const db = drizzle(pool, { schema }) -export type Db = typeof db +function connect(): { pool: Pool, db: Client } { + if (!connection) { + const pool = new Pool({ connectionString: connectionUrl() }) + + connection = { pool, db: drizzle(pool, { schema }) } + } + + return connection +} + +export const pool = new Proxy({} as Pool, { + get(_target, property) { + return Reflect.get(connect().pool, property, connect().pool) + }, +}) + +export const db = new Proxy({} as Client, { + get(_target, property) { + return Reflect.get(connect().db, property, connect().db) + }, +}) + +export type Db = Client