Configuration
The .env settings you actually change, mailboxes and SMS providers, the queue worker and scheduler, and running NetSendo behind HTTPS.
Written for NetSendo 2.7.0 and checked against its .env.example, docker-compose.yml, container entrypoint, configuration files, scheduler and DOCKER_INSTALL.md.
Where configuration lives
- The
.envfile in the installation folder, copied from.env.example. It holds the address of the instance, the database, the system mail server and switches for optional parts. Docker Compose mounts it read-only into the containers. - The panel. Mailboxes, SMS providers, AI providers, payments, integrations, API keys and list settings are made in the NetSendo interface and stored in the database.
The containers read .env when they start and the app caches its configuration at start-up, so recreate them after every change:
docker compose up -d --force-recreateEnvironment variables you change
The defaults below are those of .env.example and docker-compose.yml. Everything else in .env.example can stay as it is for a Docker installation.
| Variable | Default | What it does |
|---|---|---|
| APP_URL | http://localhost:5029 | Public address of the instance. Links in emails (unsubscribe, tracking, confirmation) and in the panel are built from it. In production: https://your-domain.com. |
| APP_KEY | (empty) | Encryption key. Generate it once before the first start (see the quick start). Mailbox passwords and provider keys are stored encrypted with it, so changing it later makes them unreadable. |
| APP_LOCALE | en | Default language of the application. |
| DB_DATABASE | netsendo_prod | Database name. |
| DB_USERNAME | netsendo_user | Database user. |
| DB_PASSWORD | root | Database password, also used as the MySQL root password. Set a strong one before the first start: the MySQL container creates the database and users only on its first start. |
| DB_PORT | 3306 | Read by the app to reach the db container and by Docker Compose to publish MySQL on 127.0.0.1. Keep 3306; if that port is taken on the host, change the published port in docker-compose.yml instead. |
| NETSENDO_VERSION | latest | Image tag of ghcr.io/netsendo/netsendo for the app, queue, scheduler and reverb services. Pin a release, for example 2.7.1, to update on your own schedule; take that release's docker-compose.yml and docker/php/docker-entrypoint.sh with it. |
| HTTP_PORT | 5029 | Port of the web server, published on 127.0.0.1 only. |
| MAILPIT_HTTP_PORT | 5031 | Port of the Mailpit web interface (127.0.0.1). |
| MAIL_MAILER, MAIL_HOST, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, MAIL_ENCRYPTION, MAIL_FROM_ADDRESS, MAIL_FROM_NAME | smtp, mailpit, 1025 | Default mail server for emails to you and your team (export ready, task reminders, CRM notices) and the fallback for a list's system emails when the list has no mailbox. The default delivers them to Mailpit, so point these at a real SMTP server in production. Campaigns do not use them; they go through the mailboxes set up in the panel. |
| VITE_REVERB_HOST, VITE_REVERB_PORT, VITE_REVERB_SCHEME | localhost, 8085, http | Where the browser connects for real-time features (Live Visitors). Behind a reverse proxy with HTTPS: your domain, 443 and https. The panel passes them to the browser when the page loads, so docker compose up -d --force-recreate applies a change; no frontend rebuild. NetSendo 2.7.0 and older ignored them in Docker and always used localhost, 8085 and http. |
| MCP_API_KEY | (empty) | API key for the optional mcp service (docker compose --profile mcp). |
| NMI_ENABLED | false | Turns on NetSendo Mail Infrastructure in the panel; see below. |
TWILIO_*, SMSAPI_*, VONAGE_*, MESSAGEBIRD_*, PLIVO_*, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_AI_API_KEY, GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are listed in .env.example, but the configuration of NetSendo 2.7.0 does not use them. Set up SMS gateways, AI providers and Google in the panel.Mailboxes
Campaigns, autoresponders and system emails are sent through mailboxes, set up under Settings → Mailboxes. NetSendo has no sending limit of its own; the volume depends on the provider behind the mailbox.
| Provider | How it sends |
|---|---|
| SMTP | Any SMTP server or relay, such as Amazon SES, Mailgun or your own server (host, port, user, password, encryption). |
| Gmail / Google Workspace | Through the Gmail API after connecting a Google account with OAuth. |
| SendGrid | Through the SendGrid API. |
| NMI | Through NetSendo Mail Infrastructure, the optional built-in MTA (see below). |
- Each mailbox is allowed to send any mix of three message types: broadcasts, autoresponders and system emails. A list can have its own default mailbox; a campaign can name one explicitly.
- A list's system emails (signup confirmation, unsubscribe confirmation) go through the list's mailbox. Without one, NetSendo uses the
MAIL_*server from.env, and if that is not usable (no host, orlocalhost), the first active SMTP mailbox allowed to send system emails. - “Forgot password” on the sign-in page does not depend on any of these: NetSendo emails a new password through its password relay service, so it works even before SMTP is set up.
- Bounces reach NetSendo by reading a bounce mailbox or through provider webhooks at
/webhooks/bounce/{provider}/{mailbox}for SendGrid, Mailgun, Postmark or a generic sender. Each request is authenticated: SendGrid and Mailgun with their own signatures, Postmark and generic senders with the per-mailbox token shown in the mailbox settings.
SMS providers
Add SMS gateways under Settings → SMS Providers: Twilio, SMSAPI (smsapi.pl), SMSAPI (smsapi.com), Vonage, MessageBird and Plivo. Each provider has a sender name and an optional daily limit, and a list can name its default provider. Without provider_id, the SMS API uses the default provider, or else the first active one that has not reached its daily limit.
Services, queue worker and scheduler
docker compose up -d starts these services. Emails, SMS, webhooks and imports are processed in the background, so queue and scheduler must run for anything to be sent.
| Service | What it does |
|---|---|
| app | The Laravel application (image ghcr.io/netsendo/netsendo). On start it runs pending database migrations and seeds missing starter data, then caches the configuration. |
| queue | Queue worker: php artisan queue:work --queue=default,notifications. Sends emails, SMS, webhooks and imports. Starts once app is healthy and the schema is up to date. |
| scheduler | php artisan schedule:work. Runs the periodic jobs listed below. Starts once app is healthy and the schema is up to date. |
| reverb | Laravel Reverb WebSocket server on port 8085 for real-time features. Waits until the schema is up to date. |
| webserver | nginx, published on 127.0.0.1:5029 (HTTP_PORT). |
| db | MySQL 8.4, data in the dbdata volume. |
| redis | Redis 7 for cache, sessions and the queue. |
| mailpit | Catches test and system emails; web interface on 127.0.0.1:5031. |
| mcp | Optional (profile mcp): MCP server for AI assistants over stdio. |
| nmi-mta | Optional (profile nmi): Haraka MTA of NetSendo Mail Infrastructure, SMTP on 127.0.0.1:2525 and submission on 127.0.0.1:2587. |
The four NetSendo containers run the same entrypoint. Since 2.7.0 it prepares the database under a MySQL lock (GET_LOCK), so only one container migrates and seeds at a time, and docker-compose.yml leaves that work to app (see start-up variables). With a compose file from an older release the containers take the lock in turn: the first one migrates, the others find nothing left to do. Watch the migrations with:
docker compose logs -f app # migrations run here; scheduler, queue and reverb wait for themWhat the scheduler runs
| Command | When and why |
|---|---|
| cron:process-queue | Every minute: sends due email campaigns and autoresponder messages |
| cron:process-sms-queue | Every minute: sends due SMS |
| automations:process-date-triggers | Every minute: date-based automation triggers |
| backup:run | Daily at 02:00: backup of the database and files (log in storage/logs/backup.log) |
| cron:daily-maintenance | Daily at 04:00: maintenance such as log cleanup |
| netsendo:check-updates | Hourly: checks for a new NetSendo release |
| license:verify --deactivate | Daily at 06:00: license check |
| netsendo:telemetry | Hourly at :17: sends the anonymous usage report once a day, only while telemetry is switched on |
| brain:index-knowledge | Hourly, with GOLD: embeds Brain knowledge base entries that have no vectors yet |
The scheduler also processes bounce and reply mailboxes, CRM follow-ups, calendar sync, funnels, NMI warming and blacklist checks, and log cleanup. Jobs of GOLD features (Campaign Auditor, Deliverability Shield DNS checks, NMI warming, blacklist checks and DKIM rotation, the autonomous Brain cron and knowledge indexing) run only while the licence includes them.
notifications queue. The docker-compose.yml of NetSendo 2.1.3 and older starts the worker for the default queue only. docker compose pull does not change your copy of the file, so edit the queue service and run docker compose up -d queue. Check the backlog first: jobs that waited will be sent at once. queue:
command: php artisan queue:work --queue=default,notifications --sleep=3 --tries=3 --max-time=3600HTTPS and a reverse proxy
The web server listens on 127.0.0.1:5029 only, so a public instance needs a reverse proxy such as nginx or Caddy in front of it, with HTTPS. Forward /app/ to Reverb on port 8085 for WebSockets.
server {
listen 443 ssl http2;
server_name netsendo.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:5029;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket proxy for Reverb
location /app/ {
proxy_pass http://127.0.0.1:8085;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
}Point the browser at the proxy: with VITE_REVERB_PORT=443 it opens wss:// on the HTTPS port, which the /app/ location forwards to Reverb.
APP_URL=https://netsendo.example.com
VITE_REVERB_HOST=netsendo.example.com
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=httpsThen recreate the containers; the panel hands the browser the new WebSocket address when a page loads (NetSendo 2.7.1 or newer):
docker compose up -d --force-recreateEmail sending options
These variables are not in .env.example, but NetSendo reads them (config/netsendo.php). The defaults suit most installations; add a line to .env only to change one.
| Variable | Default | What it does |
|---|---|---|
| EMAIL_TRANSIENT_RETRY | true | Temporary SMTP errors (4xx such as 421 Too many connections, rate limits, greylisting) are retried with a delay instead of failing. Permanent 5xx errors still fail at once. |
| EMAIL_TRANSIENT_BACKOFF_BASE | 15 | Base delay in seconds; the n-th retry waits about base × n seconds. |
| EMAIL_TRANSIENT_BACKOFF_JITTER | 15 | Random seconds added to each delay so retries do not reconnect at the same moment. |
| EMAIL_STAGGER_SENDS | true | Spreads each minute's sends across the minute instead of opening all SMTP connections at once. |
| EMAIL_STAGGER_WINDOW | 55 | Seconds over which a minute's sends are spread. |
| EMAIL_STUCK_QUEUED_MINUTES | 30 | Messages stuck as queued for longer (worker down, container restarted) are sent again. 0 turns this off. |
| EMAIL_AUTORESPONDER_BACKFILL_GRACE_MINUTES | 60 | How late an autoresponder message may still be sent to someone who never got it. Older ones need the explicit Send to missed action. |
| EMAIL_WRAP_HTML_DOCUMENT | true | Wraps HTML fragments in a full HTML document before sending, which spam filters expect. |
| EMAIL_PLAIN_TEXT_ALTERNATIVE | true | Adds a text/plain part to every email, from the message's own plain text or generated from the HTML. |
| EMAIL_LIST_UNSUBSCRIBE | true | Adds List-Unsubscribe and List-Unsubscribe-Post (RFC 8058 one-click unsubscribe, required by Gmail and Yahoo for bulk senders) to campaigns and autoresponders. |
| EMAIL_CONVERT_INLINE_IMAGES | true | Embeds images that have the class img_to_b64 as base64. |
| EMAIL_MAX_INLINE_IMAGE_SIZE | 512000 | Largest image in bytes that is embedded that way. |
| EMAIL_IMAGE_FETCH_TIMEOUT | 10 | Seconds to wait when fetching a remote image to embed. |
| BOUNCE_WEBHOOK_TIMESTAMP_TOLERANCE | 300 | Bounce webhooks from SendGrid and Mailgun are accepted only within this many seconds of their signed timestamp. |
Start-up, telemetry and hosting variables
The container entrypoint reads these two from the container environment, not from .env. Change them under environment of a service in docker-compose.yml, then run docker compose up -d.
| Variable | Default | What it does |
|---|---|---|
| NETSENDO_RUN_MIGRATIONS | true (false for scheduler, queue and reverb) | Whether the container migrates and seeds the database when it starts. With false it waits until the schema is up to date, then starts. docker-compose.yml sets false for scheduler, queue and reverb, so only app migrates. |
| NETSENDO_MIGRATIONS_WAIT_TIMEOUT | 1800 | Seconds a container waits for another one that is migrating or seeding. After that the container starts without preparing the database, and php artisan migrate or db:seed gives up instead of running a second migration. |
These are read by the application and go in .env; none of them is in .env.example. What the telemetry report contains is described in Telemetry.
| Variable | Default | What it does |
|---|---|---|
| NETSENDO_TELEMETRY | (unset) | true or false overrides the switch under Settings → Telemetry and locks it. Unset leaves the choice to the administrator in the panel, where it is off by default. |
| NETSENDO_DEPLOYMENT | (unset) | docker, cloud or other, used only in the telemetry report. Unset: docker when the container has /.dockerenv, otherwise other. |
| NETSENDO_TELEMETRY_URL | https://netsendo.com/api/telemetry | Where the telemetry report is sent. |
| NETSENDO_DORMANT | false | Set by NetSendo Cloud for an instance whose subscription has ended. While it is true, only unsubscribe links work; nothing is sent (no emails, automations or webhooks), scheduled jobs are skipped, the queue worker pauses, and every other page, the API and sign-in answer 503. Not meant for a self-hosted installation. |
NetSendo Mail Infrastructure (NMI)
NMI is an optional mail transfer agent (Haraka) for sending from your own dedicated IP addresses, with IP warming, DKIM and blacklist monitoring. Enable it in .env and start the stack with the nmi profile, then manage it in the panel under Settings → Mail Infrastructure. Adding pools and IPs, warming, DKIM keys and blacklist checks need a GOLD licence; sending through an NMI set up earlier keeps working without it.
# .env
NMI_ENABLED=true
NMI_MTA_HOST=netsendo-mta
NMI_MTA_PORT=25
# start the stack with the MTA
docker compose --profile nmi up -d| Variable | Default | What it does |
|---|---|---|
| NMI_ENABLED | false | Master switch for NMI in the panel. |
| NMI_MTA_HOST, NMI_MTA_PORT, NMI_MTA_ENCRYPTION | netsendo-mta, 25, none | Connection to the Haraka MTA container. |
| NMI_WARMING_DAYS, NMI_WARMING_AUTO_ADVANCE | 28, true | Length of the IP warming schedule and whether it advances by itself. |
| NMI_DKIM_KEY_SIZE, NMI_DKIM_ROTATION_MONTHS, NMI_DKIM_AUTO_ROTATE | 2048, 6, false | DKIM key size and rotation. |
| NMI_BLACKLIST_CHECK_HOURS, NMI_BLACKLIST_ALERT | 24, true | How often sending IPs are checked against blacklists, and whether you are alerted. |
| NMI_REPUTATION_PAUSE, NMI_REPUTATION_WARNING | 50, 70 | Reputation scores at which sending pauses or a warning appears. |
| NMI_MAX_BOUNCE_RATE, NMI_MAX_COMPLAINT_RATE | 5, 0.1 | Bounce and complaint rates (percent) above which NMI warns. |
The MTA keeps its queue, logs, DKIM keys and TLS certificates in the nmi-queue, nmi-logs, nmi-dkim and nmi-tls volumes.
Frequently asked questions
Where do I set up SMTP for NetSendo campaigns?
In the panel under Settings → Mailboxes, not in .env. A mailbox can be an SMTP server, Gmail, SendGrid or the built-in NMI, and each one can be allowed to send campaigns, autoresponders, system emails or any mix of them. The MAIL_* variables in .env are only for system emails.
Why do NetSendo notification emails end up in Mailpit?
Because .env.example sets MAIL_HOST=mailpit, so emails sent through the default mail server (export ready, task reminders, and list confirmations of lists without a mailbox) go to the Mailpit test inbox at http://localhost:5031. Point the MAIL_* variables at a real SMTP server and recreate the containers.
How do I apply a change to .env?
Run docker compose up -d --force-recreate. The containers read .env when they start and the app caches its configuration at start-up. The same applies to the VITE_REVERB_* variables since NetSendo 2.7.1; the frontend does not need a rebuild, and npm run build does not work in the production container.
Why do the NetSendo scheduler, queue and reverb containers wait when they start?
Since NetSendo 2.7.0 only the app container migrates and seeds the database. docker-compose.yml sets NETSENDO_RUN_MIGRATIONS=false for scheduler, queue and reverb, so they wait until the schema is up to date, at most NETSENDO_MIGRATIONS_WAIT_TIMEOUT seconds (1800 by default). A MySQL lock lets only one container prepare the database at a time, also with an older docker-compose.yml. Follow the migrations with docker compose logs -f app.
Why are unsubscribe confirmations or new-subscriber notifications not sent?
They use the notifications queue. Installations created with docker-compose.yml from NetSendo 2.1.3 or older start the worker for the default queue only. Change the queue service command to php artisan queue:work --queue=default,notifications --sleep=3 --tries=3 --max-time=3600 and run docker compose up -d queue.
Which SMS providers does NetSendo support?
Twilio, SMSAPI (smsapi.pl and smsapi.com), Vonage, MessageBird and Plivo. Add them in the panel under Settings → SMS Providers.
Installing for the first time? Start with the quick start. Connecting other software? See the API reference and webhooks.

