Repository navigation
Production Readiness Guide
This document covers production deployment for the Dr Necrotix Portfolio/CMS.
The repository now has a committed Prisma migration baseline, and CI verifies it against a clean PostgreSQL 16 database using prisma migrate deploy. The project also includes a production Docker image, an optional self-hosted PostgreSQL stack, environment preflight validation, and a database-backed health endpoint.
- Node.js 22-compatible runtime or Docker.
- PostgreSQL 16 or a compatible managed PostgreSQL service.
- Persistent HTTPS hostname configured through
NEXT_PUBLIC_SITE_URL. - Strong
AUTH_SECRETstored only in the deployment secret manager. - SMTP credentials when the contact form should deliver email.
- Cloudflare R2 credentials when managed media uploads are enabled.
- Optional GitHub, WakaTime, Groq, and Gemini credentials only when their related integrations are enabled.
Never commit real credentials to the repository.
Development workflow:
npm run db:validate
npm run db:generate
npm run d b:migrate
npm run db:seedProduction workflow:
npm run production:preflight
npm run db:deploy
npm run db:statusdb:migrate uses prisma migrate dev and is intended for development only. Production must use db:deploy (prisma migrate deploy). Do not use prisma db push as the production migration strategy.
The repository includes Dockerfile and docker-compose.production.yml.
Create a local production secrets file that is never committed, for example .env.production, based on .env.example. At minimum, set:
POSTGRES_PASSWORD="use-a-long-random-database-password"
AUTH_SECRET="use-a-random-secret-at-least-32-characters"
NEXT_PUBLIC_SITE_URL="https://example.com"
OWNER_NAME="Your Name"
OWNER_EMAIL="owner@example.com"
OWNER_PASSWORD="use-a-strong-password-at-least-12-characters"Then build and start the self-hosted stack:
docker compose --env-file .env.production -f docker-compose.production.yml up -d --buildThe app listens only on 127.0.0.1:3000 by default, so expose it publicly through a TLS reverse proxy such as Caddy, Nginx, Traefik, or your hosting provider/CDN.
On container startup, the application runs production environment validation and prisma migrate deploy before starting Next.js.
To inspect the stack:
docker compose --env-file .env.production -f docker-compose.production.yml ps
docker compose --env-file .env.production -f docker-compose.production.yml logs -f appTo stop it without deleting the PostgreSQL volume:
docker compose --env-file .env.production -f docker-compose.production.yml downDo not use down -v unless you intentionally want to delete the database volume.
For a managed database, deploy only the application container and set DATABASE_URL to the provider's PostgreSQL connection string. Run:
npm run production:preflight
npm run db:deploy
npm run db:statusBefore serving production traffic. Prefer provider-side automated backups and point-in-time recovery when available.
The initial OWNER account is created by prisma/seed.cjs using OWNER_NAME, OWNER_EMAIL, and OWNER_PASSWORD.
Run once after the production database is migrated:
npm run db:seedThe password must be at least 12 characters. The seed uses an upsert, so running it again updates the configured owner account rather than creating duplicate owner records.
After bootstrap, rotate or remove OWNER_PASSWORD from the long-lived deployment environment when operationally practical.
Production must use HTTPS. AUTH_SECRET must be at least 32 characters and should be generated from cryptographically secure random data. Do not rotate it casually because it participates in session and private Site Mode security.
Store production secrets in the hosting platform's secret manager or a root-readable environment file outside the repository. Never place secrets in Docker images, Git history, screenshots, issue comments, or CI logs.
The production preflight command fails startup when core variables are missing, the canonical URL is not HTTPS, AUTH_SECRET is too short, or SMTP/R2 is only partially configured.
GET /api/health performs a lightweight database query and returns 200 when the application and PostgreSQL connection are healthy or 503 when the database is unavailable. Responses are no-store and expose no credentials or raw database errors.
The Docker image uses this endpoint for its container health check. External uptime monitoring may use the same endpoint.
Before maintenance, migration, or risky content operations, use the CMS Site Mode controls rather than modifying public frontend routes.
Available modes:
-
NORMAL— standard public site. -
MAINTENANCE— public maintenance gate. -
COMING_SOON— launch/holding gate. -
PRIVATE— password-protected public access. -
ARCHIVE— public read-only mode for supported public writes.
Verify the selected mode from a logged-out browser before and after a production maintenance window.
When R2 is enabled, configure all of these together:
R2_ACCOUNT_ID=""
R2_ACCESS_KEY_ID=""
R2_SECRET_ACCESS_KEY=""
R2_BUCKET=""
R2_PUBLIC_BASE_URL="https://media.example.com"Use a dedicated bucket or tightly scoped credentials. Keep bucket/versioning or an independent backup strategy because database backups do not contain R2 objects.
Configure all required SMTP fields together:
EMAIL_USER=""
EMAIL_APP_PASSWORD=""
SMTP_HOST="smtp.example.com"
SMTP_PORT="465"
SMTP_SECURE="true"Before launch, submit a real contact-form test through the production hostname and verify delivery, sender configuration, spam controls, and reverse-proxy client IP behavior.
GITHUB_TOKEN=""
WAKATIME_API_KEY=""
GROQ_API_KEY=""
GEMINI_API_KEY=""Use the minimum token permissions required. Monitor AI API cost and abuse in production even though the public chat endpoint already includes input limits, throttling, upstream timeouts, and CMS-grounded context.
The public origin must use HTTPS and must match NEXT_PUBLIC_SITE_URL. Forward standard proxy headers (Host, X-Forwarded-For, X-Forwarded-Proto) so absolute URLs, authentication, and rate-limiting behavior remain correct.
A typical topology is:
Internet / Cloudflare
|
HTTPS reverse proxy
|
127.0.0.1:3000 (Portfolio)
|
PostgreSQL
Do not publish PostgreSQL port 5432 directly to the public Internet.
Before deployment:
npm install --no-audit --no-fund
npm run production:preflight
npm run db:validate
npm run typecheck
npm run lint
npm run build
npm run test:visual
npm run db:statusDeploy only a commit whose exact-head GitHub Actions CI is green.
Before schema migrations or major CMS changes:
- create a PostgreSQL backup/snapshot;
- confirm that a restore procedure has been tested;
- retain a separate backup/versioning strategy for R2 media;
- record the deployed Git commit SHA;
- keep the previous application image/release available for rollback.
For the self-hosted Compose database, schedule pg_dump backups outside the application container and copy them to storage outside the server.
- Enable
MAINTENANCESite Mode for risky database/infrastructure changes when appropriate. - Take a database backup.
- Pull/build the exact green commit.
- Run production preflight.
- Apply
npm run db:deploy. - Verify
npm run db:status. - Start/restart the application.
- Verify
/api/healthreturns HTTP 200. - Verify public Homepage, Projects, Blog, and Contact.
- Verify
/adminlogin and one non-destructive CMS read operation. - Test R2/SMTP if configured.
- Restore
NORMALSite Mode and re-check from a logged-out browser.
Application rollback should redeploy the previous known-good Git SHA/container image. Database migrations require more care: Prisma migrations are forward-oriented, so do not improvise destructive SQL rollback. Restore from a tested database backup when a schema migration cannot be safely corrected with a new forward migration.
- Production PostgreSQL is provisioned and not publicly exposed.
- Committed Prisma migrations deploy successfully to an empty database.
-
npm run db:statusreports the database up to date. - OWNER seed succeeds.
-
AUTH_SECRETis unique, at least 32 characters, and stored as a secret. - Canonical HTTPS site URL is configured.
- Reverse proxy/CDN forwards HTTPS and client headers correctly.
-
/api/healthreturns 200. - SMTP delivery is tested if enabled.
- R2 upload/read/delete behavior is tested if enabled.
- Optional integration tokens use minimum permissions.
- Exact-head GitHub Actions CI is green.
- Database and R2 backup/restore procedures exist.
- Homepage, Projects, Blog, Contact,
/admin, Site Modes, and Day/Night presentation are checked after deployment.
Production work must not use infrastructure changes as a reason to redesign the protected visual surfaces. The protected Homepage identity/Hero, Navigation, Projects archive, Blog archive, profile treatment, selected animations, responsive behavior, and Day/Night presentation remain a design contract. Backend and deployment changes must adapt to those surfaces, not replace them.