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-appCreate .env
cp .env.docker .envEdit .env and set at least:
ADMIN_EMAIL: an address you can read. It becomes the first admin account.POSTGRES_PASSWORDandMINIO_ROOT_PASSWORD: strong values for anything beyond a laptop. Both default tochangeme.RESEND_API_KEYandEMAIL_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 -dWatch 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_KEYIf 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 examplehttps://crm.example.com. It setsBETTER_AUTH_URLandNEXT_PUBLIC_APP_URL, which auth and the links in emails use. Defaulthttp://localhost:3000.MINIO_PUBLIC_URL: the address browsers use to reach MinIO, for examplehttps://files.example.com. Uploads go straight from the browser to MinIO with a presigned URL signed for this address, and file links point here. Defaulthttp://localhost:9000.
What runs
| Service | Image | Exposed to the host |
|---|---|---|
app | built from Dockerfile | port 3000 |
postgres | pgvector/pgvector:pg17 | no |
minio | pgsty/minio | 127.0.0.1:9000 |
inngest | inngest/inngest:latest, inngest start | no |
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:
- Waits up to 30 seconds for Postgres.
- Loads
BETTER_AUTH_SECRET,EMAIL_ENCRYPTION_KEY,INNGEST_SIGNING_KEYandINNGEST_EVENT_KEYfrom the environment, or from/app/data/secrets, generating and saving any that are missing (see Secrets). - Runs
prisma migrate deploy. - 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. - Counts rows in the
Userstable. If there are none, runsprisma db seed. - 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 volumesData 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.