NextCRM

Install with Docker Compose

Run NextCRM with the bundled Postgres, MinIO and Inngest, then sign in as the first admin.

The repository ships a docker-compose.yml that runs the whole stack. On first start the container applies database migrations, creates the storage bucket and seeds the first admin account.

Quick start

Get the code

git clone https://github.com/pdovhomilja/nextcrm-app.git
cd nextcrm-app

Create .env

cp .env.docker .env

Edit .env and set at least:

  • ADMIN_EMAIL: an address you can read. It becomes the first admin account.
  • POSTGRES_PASSWORD and MINIO_ROOT_PASSWORD: strong values for anything beyond a laptop. Both default to changeme.
  • RESEND_API_KEY and EMAIL_FROM, so login codes can be emailed. For a quick local test you can skip them and read the code from the database (see First login).

On a server, also set APP_URL and MINIO_PUBLIC_URL (see Public URLs).

Start

docker compose up -d

Watch the startup with docker compose logs -f app. Open http://localhost:3000 when the log shows Starting NextCRM....

Which variables reach the app

Docker Compose uses .env to fill in the ${...} placeholders in docker-compose.yml, and the app container receives the variables listed under services.app.environment. That list covers every variable in .env.docker: database, MinIO, Inngest, auth secrets, email (Resend and SMTP), AI keys, Google login and calendar, Firecrawl, E2B, NEXTCRM_TOKEN and the advanced switches. See the configuration reference for what each one does.

Optional API keys left empty stay empty, so keys you save in the admin panel are used. A value set in .env always wins over the admin panel.

Secrets

BETTER_AUTH_SECRET, EMAIL_ENCRYPTION_KEY, INNGEST_SIGNING_KEY and INNGEST_EVENT_KEY do not have to be set. On the first start the entrypoint generates the missing ones and saves them in the app_data volume (/app/data/secrets); later starts read them from there. A value set in .env always wins over the saved file.

Never change BETTER_AUTH_SECRET or EMAIL_ENCRYPTION_KEY once the instance is in use. A new BETTER_AUTH_SECRET logs all users out. A new EMAIL_ENCRYPTION_KEY makes everything encrypted with the old key unreadable: AI provider keys saved in the admin panel, mailbox passwords, Google Calendar tokens, Calendly settings and plugin secrets. Back up the app_data volume together with the database, or set the values in .env yourself and back up that file.

To manage the secrets yourself, generate them once and put them in .env before the first start:

openssl rand -base64 32   # BETTER_AUTH_SECRET
openssl rand -hex 32      # EMAIL_ENCRYPTION_KEY (must be 64 hex characters)
openssl rand -hex 32      # INNGEST_SIGNING_KEY (hex)
openssl rand -hex 16      # INNGEST_EVENT_KEY

If the entrypoint cannot write to /app/data and a secret is missing, the container stops with an error instead of starting with a throwaway value.

Public URLs

Two URLs must be reachable from your users' browsers:

  • APP_URL: the address people open, for example https://crm.example.com. It sets BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL, which auth and the links in emails use. Default http://localhost:3000.
  • MINIO_PUBLIC_URL: the address browsers use to reach MinIO, for example https://files.example.com. Uploads go straight from the browser to MinIO with a presigned URL signed for this address, and file links point here. Default http://localhost:9000.

What runs

ServiceImageExposed to the host
appbuilt from Dockerfileport 3000
postgrespgvector/pgvector:pg17no
miniopgsty/minio127.0.0.1:9000
inngestinngest/inngest:latest, inngest startno

MinIO's S3 port is published on 127.0.0.1 only, which is enough for a browser on the same machine. On a server, route a domain to it through a reverse proxy and set MINIO_PUBLIC_URL. Each internal service has a commented-out ports: entry in docker-compose.yml if you need direct access (5432 for Postgres, 9001 for the MinIO console, 8288 for the Inngest dashboard).

Inngest runs as a self-hosted server in production mode: every request between it and /api/inngest is signed with INNGEST_SIGNING_KEY, so nobody can trigger jobs through the public endpoint. See Background jobs.

What happens on start

docker-entrypoint.sh runs these steps every time the app container starts:

  1. Waits up to 30 seconds for Postgres.
  2. Loads BETTER_AUTH_SECRET, EMAIL_ENCRYPTION_KEY, INNGEST_SIGNING_KEY and INNGEST_EVENT_KEY from the environment, or from /app/data/secrets, generating and saving any that are missing (see Secrets).
  3. Runs prisma migrate deploy.
  4. Creates the MinIO bucket if it does not exist. If the bucket has no policy yet, it allows anonymous reads of the upload folders (avatars/, images/, documents/, uploads/, thumbnails/), because file links in the app are plain URLs. Invoices stay private and are served through short-lived presigned links. A policy you set yourself is left alone.
  5. Counts rows in the Users table. If there are none, runs prisma db seed.
  6. Starts the server with node server.js.

The seed fills default CRM values (industries, lead sources, sales stages and similar) and creates the admin user.

First login

The seed creates one user with the email from ADMIN_EMAIL, role admin, status active and the display name "Admin". Change the name in your profile after you sign in. ADMIN_EMAIL is read only on the first start; changing it later has no effect.

There is no password. On the sign-in page, enter the admin email and request a code.

Set RESEND_API_KEY and EMAIL_FROM (a sender address verified in Resend) and make sure both reach the container. The code arrives by email and expires after 5 minutes.

Once you are in, open Administration (/admin) and continue with Users and roles.

Stopping and data

docker compose down      # stops containers, keeps data
docker compose down -v   # stops containers and deletes all data volumes

Data lives in four named volumes: postgres_data (database), minio_data (uploaded files), app_data (generated secrets) and inngest_data (queued and scheduled jobs). See Backups.

On this page