Files
linkder/apps/web/next.config.ts
serfaandClaude Opus 5 35d99ce0e2 Containerise for Dokploy, and a demo login that survives production
Everything needed to build and run this on Dokploy at
linkdr.serfaty.site, plus the two things that turned out to be broken
the moment it left a laptop.

The build did not work in a container at all. `lib/auth.ts` throws when
AUTH_SECRET or NEXT_PUBLIC_APP_URL is missing — correct at boot, wrong
during `next build`, which imports every route module with
NODE_ENV=production and none of the runtime secrets. The only way past
it was baking a session key into an image layer, which is worse than
the problem the guard exists to prevent. Both checks now skip
NEXT_PHASE=phase-production-build and still fire on a real boot.

Corepack in node:22.12-alpine ships expired npm registry signing keys
and dies before it can download pnpm, so the image installs corepack
first and prepares the pinned version explicitly.

The image is the standalone trace, which needs outputFileTracingRoot at
the REPO root: pnpm hoists to a root .pnpm store and tracing from
apps/web silently omits every workspace package. 427MB, runs as
non-root, and its healthcheck talks to Postgres — a container that
cannot reach its database must never enter rotation, because a deploy
that goes green and then 500s does not roll back.

DEMO_LOGIN is a login bypass under NODE_ENV=production and there is no
honest way to describe it otherwise. It is a separate variable from
ALLOW_DEV_LOGIN so that copying a dev .env into a real environment
cannot enable it by accident, it still only affects the one seeded
number, and it prints a boot warning every single start so it cannot be
forgotten. That deployment holds nothing but fixtures. It comes out
before the platform sees a real signup.

Also: /api/health, and next/image hosts corrected to the Spaces bucket
rather than the R2 one this stopped using.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:17:54 -04:00

66 lines
2.9 KiB
TypeScript

import path from 'node:path';
import { config as loadEnv } from 'dotenv';
import { withSentryConfig } from '@sentry/nextjs';
import type { NextConfig } from 'next';
// The monorepo keeps one .env at the root; Next only looks in the app directory.
// In a container the file does not exist and the platform supplies the
// environment instead — dotenv never overwrites an already-set variable, so
// this line is a no-op there rather than a conflict.
loadEnv({ path: '../../.env' });
const config: NextConfig = {
reactStrictMode: true,
/**
* Traces the server build and its used dependencies into
* `.next/standalone`, so the runtime image carries a node_modules with only
* what actually runs. Without it a Docker image for this monorepo has to ship
* every workspace's dev dependencies — drizzle-kit, vitest, eslint, the whole
* toolchain — to start one server.
*
* `outputFileTracingRoot` must point at the REPO root, not the app: pnpm
* hoists to a root `node_modules/.pnpm` store, and tracing from apps/web
* silently omits every symlinked workspace package.
*/
output: 'standalone',
outputFileTracingRoot: path.join(__dirname, '../..'),
// The workspace packages ship TypeScript source, not build output.
transpilePackages: ['@linkdr/api', '@linkdr/db', '@linkdr/shared', '@linkdr/storage'],
images: {
remotePatterns: [
// Where everything is served from once `pnpm assets:migrate` has run.
{ protocol: 'https', hostname: '**.digitaloceanspaces.com' },
{ protocol: 'https', hostname: '**.cdn.digitaloceanspaces.com' },
// The seed writes source urls and the migration rewrites them, so a
// freshly seeded environment points here until that job has run.
{ protocol: 'https', hostname: 'images.unsplash.com' },
],
},
// postgres-js opens raw sockets; it must not be bundled into the server chunk.
serverExternalPackages: ['postgres'],
};
/**
* Bugsink speaks the Sentry protocol, so the Sentry build plugin applies — but
* only the parts that make sense for a self-hosted error tracker.
*
* Source maps are uploaded so a minified production stack is readable, and then
* deleted from the build output so they are not served publicly. Everything
* tracing-related stays off: Bugsink does not ingest it.
*/
export default withSentryConfig(config, {
// Bugsink has no organisation/project slugs in the Sentry sense; the DSN
// carries the project. These are only used by the upload step, which is
// skipped entirely without an auth token.
silent: true,
disableLogger: true,
sourcemaps: {
deleteSourcemapsAfterUpload: true,
},
// Do NOT route events through a Next rewrite: the tunnel exists to dodge ad
// blockers against sentry.io, and this DSN is our own host already.
tunnelRoute: undefined,
// The SDK's automatic Vercel Cron instrumentation has nothing to talk to here.
automaticVercelMonitors: false,
});