NetSendo

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 .env file 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-recreate

Environment 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.

VariableDefaultWhat it does
APP_URLhttp://localhost:5029Public 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_LOCALEenDefault language of the application.
DB_DATABASEnetsendo_prodDatabase name.
DB_USERNAMEnetsendo_userDatabase user.
DB_PASSWORDrootDatabase 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_PORT3306Read 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_VERSIONlatestImage 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_PORT5029Port of the web server, published on 127.0.0.1 only.
MAILPIT_HTTP_PORT5031Port 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_NAMEsmtp, mailpit, 1025Default 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_SCHEMElocalhost, 8085, httpWhere 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_ENABLEDfalseTurns on NetSendo Mail Infrastructure in the panel; see below.
Variables in .env.example that 2.7.0 does not read
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.

ProviderHow it sends
SMTPAny SMTP server or relay, such as Amazon SES, Mailgun or your own server (host, port, user, password, encryption).
Gmail / Google WorkspaceThrough the Gmail API after connecting a Google account with OAuth.
SendGridThrough the SendGrid API.
NMIThrough 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, or localhost), 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.

ServiceWhat it does
appThe Laravel application (image ghcr.io/netsendo/netsendo). On start it runs pending database migrations and seeds missing starter data, then caches the configuration.
queueQueue 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.
schedulerphp artisan schedule:work. Runs the periodic jobs listed below. Starts once app is healthy and the schema is up to date.
reverbLaravel Reverb WebSocket server on port 8085 for real-time features. Waits until the schema is up to date.
webservernginx, published on 127.0.0.1:5029 (HTTP_PORT).
dbMySQL 8.4, data in the dbdata volume.
redisRedis 7 for cache, sessions and the queue.
mailpitCatches test and system emails; web interface on 127.0.0.1:5031.
mcpOptional (profile mcp): MCP server for AI assistants over stdio.
nmi-mtaOptional (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 them

What the scheduler runs

CommandWhen and why
cron:process-queueEvery minute: sends due email campaigns and autoresponder messages
cron:process-sms-queueEvery minute: sends due SMS
automations:process-date-triggersEvery minute: date-based automation triggers
backup:runDaily at 02:00: backup of the database and files (log in storage/logs/backup.log)
cron:daily-maintenanceDaily at 04:00: maintenance such as log cleanup
netsendo:check-updatesHourly: checks for a new NetSendo release
license:verify --deactivateDaily at 06:00: license check
netsendo:telemetryHourly at :17: sends the anonymous usage report once a day, only while telemetry is switched on
brain:index-knowledgeHourly, 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.

The queue worker must read the notifications queue
Unsubscribe confirmations and new-subscriber notifications use the 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=3600

HTTPS 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.

nginx
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.

.env
APP_URL=https://netsendo.example.com
VITE_REVERB_HOST=netsendo.example.com
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=https

Then 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-recreate

Email 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.

VariableDefaultWhat it does
EMAIL_TRANSIENT_RETRYtrueTemporary 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_BASE15Base delay in seconds; the n-th retry waits about base × n seconds.
EMAIL_TRANSIENT_BACKOFF_JITTER15Random seconds added to each delay so retries do not reconnect at the same moment.
EMAIL_STAGGER_SENDStrueSpreads each minute's sends across the minute instead of opening all SMTP connections at once.
EMAIL_STAGGER_WINDOW55Seconds over which a minute's sends are spread.
EMAIL_STUCK_QUEUED_MINUTES30Messages stuck as queued for longer (worker down, container restarted) are sent again. 0 turns this off.
EMAIL_AUTORESPONDER_BACKFILL_GRACE_MINUTES60How 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_DOCUMENTtrueWraps HTML fragments in a full HTML document before sending, which spam filters expect.
EMAIL_PLAIN_TEXT_ALTERNATIVEtrueAdds a text/plain part to every email, from the message's own plain text or generated from the HTML.
EMAIL_LIST_UNSUBSCRIBEtrueAdds 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_IMAGEStrueEmbeds images that have the class img_to_b64 as base64.
EMAIL_MAX_INLINE_IMAGE_SIZE512000Largest image in bytes that is embedded that way.
EMAIL_IMAGE_FETCH_TIMEOUT10Seconds to wait when fetching a remote image to embed.
BOUNCE_WEBHOOK_TIMESTAMP_TOLERANCE300Bounce 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.

VariableDefaultWhat it does
NETSENDO_RUN_MIGRATIONStrue (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_TIMEOUT1800Seconds 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.

VariableDefaultWhat 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_URLhttps://netsendo.com/api/telemetryWhere the telemetry report is sent.
NETSENDO_DORMANTfalseSet 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
VariableDefaultWhat it does
NMI_ENABLEDfalseMaster switch for NMI in the panel.
NMI_MTA_HOST, NMI_MTA_PORT, NMI_MTA_ENCRYPTIONnetsendo-mta, 25, noneConnection to the Haraka MTA container.
NMI_WARMING_DAYS, NMI_WARMING_AUTO_ADVANCE28, trueLength of the IP warming schedule and whether it advances by itself.
NMI_DKIM_KEY_SIZE, NMI_DKIM_ROTATION_MONTHS, NMI_DKIM_AUTO_ROTATE2048, 6, falseDKIM key size and rotation.
NMI_BLACKLIST_CHECK_HOURS, NMI_BLACKLIST_ALERT24, trueHow often sending IPs are checked against blacklists, and whether you are alerted.
NMI_REPUTATION_PAUSE, NMI_REPUTATION_WARNING50, 70Reputation scores at which sending pauses or a warning appears.
NMI_MAX_BOUNCE_RATE, NMI_MAX_COMPLAINT_RATE5, 0.1Bounce 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.