NextCRM

Deployment

Run NextCRM on Coolify, Portainer or a build-pack platform, behind a reverse proxy with HTTPS.

Any platform that can run a Docker Compose stack can run NextCRM. The repository has two compose files:

FileUse it for
docker-compose.ymlA local trial or a single host. Publishes port 3000 (and MinIO on 127.0.0.1:9000), has defaults for every password, generates missing secrets on the first start.
docker-compose-coolify.ymlPlatforms that inject environment variables and route traffic through their own proxy. Every deployment-specific value is a ${VAR}, required secrets fail the deploy if missing, no host ports are published.

Both pass the same set of variables to the app (see the configuration reference) and run Inngest in signed production mode.

Coolify

Create the resource

Point Coolify at the repository and choose the Docker Compose build pack with compose location /docker-compose-coolify.yml.

Set the required variables

The deploy fails without these four:

  • POSTGRES_PASSWORD
  • MINIO_ROOT_PASSWORD
  • BETTER_AUTH_SECRET (openssl rand -base64 32)
  • EMAIL_ENCRYPTION_KEY (openssl rand -hex 32)

Set them once and never change them. A new BETTER_AUTH_SECRET logs everyone out. A new EMAIL_ENCRYPTION_KEY makes stored keys and mailbox passwords unreadable.

INNGEST_SIGNING_KEY and INNGEST_EVENT_KEY are optional. When they are empty, the app generates them on the first start, stores them in the app_data volume and shares them with the bundled Inngest server.

Set the instance URL and email

  • APP_URL: the public URL, for example https://crm.example.com. The compose file uses it for both NEXT_PUBLIC_APP_URL and BETTER_AUTH_URL.
  • ADMIN_EMAIL: the first admin's address.
  • RESEND_API_KEY and EMAIL_FROM (a verified Resend sender), so login codes can be sent.

Add optional keys from the configuration reference as needed; docker-compose-coolify.yml passes all of them. Leave an API key empty to use the one saved in the admin panel.

Assign domains

Assign your domain to the app service, port 3000. Assign a second domain to the minio service, port 9000, and set MINIO_PUBLIC_URL to it, for example https://files.example.com. Browsers upload to and download from that address (see File storage behind a proxy).

Portainer, Dockge and similar

Paste a compose file into a new stack and set variables in the platform UI. Variables set there fill the ${...} placeholders the same way a .env file does.

  • docker-compose.yml works as is. Set APP_URL and MINIO_PUBLIC_URL to your public URLs.
  • docker-compose-coolify.yml requires the secrets up front and uses expose instead of ports. Add ports: entries to the app and minio services, or attach your reverse proxy to the stack's network.

Build-pack platforms

nixpacks.toml makes Nixpacks (used by Coolify's Nixpacks build pack) install a current pnpm. Platforms that build from source run the package scripts:

  • pnpm build runs prisma generate, prisma migrate deploy and next build. Migrations therefore run during the build, and the build needs DATABASE_URL pointing at a reachable database.
  • pnpm start runs next start.

In this mode nothing is bundled. You provide PostgreSQL with pgvector, S3-compatible storage and an Inngest server yourself, set every required variable, and seed the first admin once with pnpm prisma db seed (it uses TEST_USER_EMAIL).

Reverse proxy and HTTPS

The app listens on plain HTTP, port 3000. Terminate TLS in front of it (Coolify's proxy, Traefik, Caddy, nginx) and forward to the app.

  • Set BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL to the public https:// URL. Email links, the Google sign-in and Calendar redirect URIs, and the Calendly webhook URL are built from them.
  • Some responses are streamed: the interactive enrichment endpoints use server-sent events, and the MCP server offers an SSE transport at /api/mcp/sse. Turn off response buffering for the app if your proxy buffers by default.
  • Keep Postgres, MinIO's console and the Inngest dashboard off the public internet.

File storage behind a proxy

Browsers talk to object storage directly:

  • Uploads. The app returns a presigned PUT URL, and the browser uploads the file to it.
  • Downloads and previews. File links are built as <public endpoint>/<bucket>/<key>, and the browser loads them from there. Invoice PDFs are served through short-lived presigned links instead.

Both compose files connect the app to MinIO at http://minio:9000, an address only containers can resolve, and sign every browser-facing URL for MINIO_PUBLIC_URL (passed to the app as MINIO_PUBLIC_ENDPOINT). For uploads and file links to work:

  1. Publish MinIO's S3 port (9000) under a domain browsers can reach, for example https://files.example.com.
  2. Set MINIO_PUBLIC_URL to that URL. The app does not need to reach it itself.

The entrypoint creates the bucket (it works with http:// and https:// endpoints) and, if the bucket has no policy yet, allows anonymous reads of the upload folders avatars/, images/, documents/, uploads/ and thumbnails/. Object keys are random UUIDs. Invoices stay private. If you manage the bucket policy yourself, the entrypoint leaves it alone; grant anonymous s3:GetObject on those folders, or file links will not open.

Migrations on start

With the Dockerfile, the entrypoint runs prisma migrate deploy on every start, before the server starts. If a migration fails, the container exits and the platform restarts it; check the app logs. The seed runs only when the Users table is empty.

The Dockerfile build generates the plugin registry (scripts/plugins/generate-registry.mjs), runs prisma generate and next build with placeholder values. Real secrets are only needed at runtime.

On this page