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:
| File | Use it for |
|---|---|
docker-compose.yml | A 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.yml | Platforms 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_PASSWORDMINIO_ROOT_PASSWORDBETTER_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 examplehttps://crm.example.com. The compose file uses it for bothNEXT_PUBLIC_APP_URLandBETTER_AUTH_URL.ADMIN_EMAIL: the first admin's address.RESEND_API_KEYandEMAIL_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.ymlworks as is. SetAPP_URLandMINIO_PUBLIC_URLto your public URLs.docker-compose-coolify.ymlrequires the secrets up front and usesexposeinstead ofports. Addports:entries to theappandminioservices, 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 buildrunsprisma generate,prisma migrate deployandnext build. Migrations therefore run during the build, and the build needsDATABASE_URLpointing at a reachable database.pnpm startrunsnext 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_URLandNEXT_PUBLIC_APP_URLto the publichttps://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
PUTURL, 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:
- Publish MinIO's S3 port (9000) under a domain browsers can reach, for example
https://files.example.com. - Set
MINIO_PUBLIC_URLto 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.