Notifuse is a self-hosted email platform replacing Mailchimp with a visual MJML builder, multi-provider delivery routing, and zero per-contact fees.
French developer Pierre-B started Notifuse as a hosted email tool in 2016 before rebuilding it from scratch in Go and React for teams seeking direct ownership over their delivery pipeline. Today, it bridges visual marketing newsletters and developer-driven transactional delivery from a single self-hosted control plane. That combination allows agencies and operational teams to manage client lists, templates, and outbound delivery without recurring per-subscriber SaaS fees.
Mailchimp isolates marketing newsletters from transactional notifications, forcing teams into separate billing tiers, API keys, and disconnected template workflows under Mandrill. As audience lists grow, its per-contact pricing climbs regardless of email volume, turning inactive subscribers into recurring overhead. Notifuse converges marketing campaigns and transactional triggers inside one dashboard backed by your own delivery infrastructure. Instead of paying vendor markups on contact counts, you pay wholesale delivery rates directly to providers like Amazon SES, Mailgun, or Postmark.
The workflow also shifts how non-technical staff and developers collaborate. Marketing teams retain a drag-and-drop MJML builder with visual preview and Liquid tags, while developers trigger templated messages through a REST API using the same assets. The trade-off is infrastructure responsibility: Mailchimp manages sender reputation and shared IP pools, whereas Notifuse requires you to configure DNS records, verify domains, and handle bounces on your chosen provider.
Can I self-host Notifuse with Docker? Yes. The project provides official Docker images and compose manifests configured for a Go backend, React console, and PostgreSQL database.
Notifuse vs Mailchimp — which should a small team choose? Choose Mailchimp if you want an all-in-one hosted service that manages IP reputation and email deliverability automatically. Pick Notifuse if you have existing cloud infrastructure, prefer wholesale delivery rates via Amazon SES, and want full control over your subscriber data.
Which email delivery providers does Notifuse support? Notifuse includes native integrations for Amazon SES, Mailgun, Postmark, Mailjet, SparkPost, and standard SMTP relays.
Notifuse is an effective self-hosted platform for agencies and engineering teams that want to eliminate per-contact SaaS fees by connecting their own delivery infrastructure, provided they are prepared to handle DNS records and deliverability monitoring. Active maintenance through regular commits and its source-available Business Source License ensure modern tooling, though teams needing enterprise single sign-on or more than three workspaces should account for commercial licensing fees.
We started this exact setup from scratch in a clean Linux machine on Nov 3, 2026 and checked it through Notifuse's own API: every check passed. Recipes are re-checked regularly and removed if they stop working.
mkdir -p notifuse && cd notifuse
curl -fsSL https://open-source.store/tools/notifuse/compose.yml -o compose.ymlcat > .env <<EOF
SECRET_KEY=$(openssl rand -base64 32)
POSTGRES_PASSWORD=$(openssl rand -hex 12)
API_ENDPOINT=http://localhost:8080
EOFSECRET_KEY — 32-byte base64 encryption key for stored credentials, session tokens, and API keys. Generated above; keep it secret.POSTGRES_PASSWORD — Password for the PostgreSQL database user. Generated above; keep it secret.API_ENDPOINT — Public URL of the Notifuse instance used for links and redirects.docker compose up -dservices:
api:
image: notifuse/notifuse:v42.1
ports:
- "8080:8080"
environment:
- SERVER_PORT=8080
- SERVER_HOST=0.0.0.0
- ENVIRONMENT=production
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=postgres
- DB_PASSWORD=${POSTGRES_PASSWORD}
- DB_NAME=notifuse_system
- DB_SSLMODE=disable
- SECRET_KEY=${SECRET_KEY}
- API_ENDPOINT=${API_ENDPOINT}
depends_on:
postgres:
condition: service_healthy
volumes:
- notifuse-data:/app/data
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/healthz"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
postgres:
image: postgres:17-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=postgres
volumes:
- postgres-data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
notifuse-data:
postgres-data:
Free IDE for Java, Kotlin, Go, TypeScript and Python. Built on IntelliJ Community, fully open source, 300+ plugins.
Show that Notifuse is listed as an open-source alternative. Paste into your README:
Ubuntu 24.04 · Apple container, Docker 29.1.3, Compose 2.40.3+ds1-0ubuntu1~24.04.1. Images pulled in 36 s: postgres:17-alpine, notifuse/notifuse:v42.1.
Based on the upstream setup. Upstream compose with notifuse/notifuse image pinned to v42.1; database password and secret key moved to .env.
api-1 | 2026-11-03T12:33:24.202512005Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Task scheduler will start in 30 seconds..."}
api-1 | 2026-11-03T12:33:24.202520297Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Email queue worker will start in 30 seconds..."}
api-1 | 2026-11-03T12:33:24.202533880Z {"level":"info","delay":30000000000,"time":"2026-11-03T12:33:24Z","message":"Automation scheduler will start after delay..."}
api-1 | 2026-11-03T12:33:24.273412505Z {"level":"info","email":"[email protected]","time":"2026-11-03T12:33:24Z","message":"Setup wizard completed successfully"}
api-1 | 2026-11-03T12:33:24.774661797Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Setup completed - initiating graceful shutdown for configuration reload"}
api-1 | 2026-11-03T12:33:24.774981339Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Starting graceful shutdown..."}
api-1 | 2026-11-03T12:33:24.774983547Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Stopping blog cache..."}
api-1 | 2026-11-03T12:33:24.774984339Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Server shutdown initiated during webhook worker delay, worker will not start"}
api-1 | 2026-11-03T12:33:24.774985130Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Server shutdown during automation scheduler delay"}
api-1 | 2026-11-03T12:33:24.774985797Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Server shutdown initiated during email queue worker delay, worker will not start"}
api-1 | 2026-11-03T12:33:24.774992714Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Server shutdown initiated during task scheduler delay, scheduler will not start"}
api-1 | 2026-11-03T12:33:24.774995964Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Flushing web analytics buffer..."}
api-1 | 2026-11-03T12:33:24.774996630Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Stopping automation scheduler..."}
api-1 | 2026-11-03T12:33:24.774997380Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Stopping email queue worker..."}
api-1 | 2026-11-03T12:33:24.774998005Z {"level":"info","active_requests":0,"time":"2026-11-03T12:33:24Z","message":"Active requests at shutdown start"}
api-1 | 2026-11-03T12:33:24.774998714Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"Waiting for active requests to complete..."}
api-1 | 2026-11-03T12:33:24.774999339Z {"level":"info","time":"2026-11-03T12:33:24Z","message":"All requests completed"}
api-1 | 2026-11-03T12:33:24.774999922Z {"level":"info","timeout":60000000000,"time":"2026-11-03T12:33:24Z","message":"Starting HTTP server shutdown"}
api-1 | 2026-11-03T12:33:24.775000630Z {"level":"error","error":"http: Server closed","time":"2026-11-03T12:33:24Z","message":"Server error"}
api-1 | 2026-11-03T12:33:25.005936339Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Starting API server on 0.0.0.0:8080"}
api-1 | 2026-11-03T12:33:25.005982672Z {"level":"info","version":"42.1","time":"2026-11-03T12:33:25Z","message":"Starting Notifuse application"}
api-1 | 2026-11-03T12:33:25.005998255Z {"level":"info","max_active_contacts":"unlimited","max_stored_contacts":"unlimited","max_monthly_events":"unlimited","max_monthly_pageviews":"unlimited","data_retention_months":"unlimited","time":"2026-11-03T12:33:25Z","message":"Plan limits resolved"}
api-1 | 2026-11-03T12:33:25.006009630Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Connecting to database postgres:5432, user postgres, sslmode disable, password: C...e, dbname: notifuse_system"}
api-1 | 2026-11-03T12:33:25.009463214Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"System database check completed"}
api-1 | 2026-11-03T12:33:25.019120214Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Starting migration process"}
api-1 | 2026-11-03T12:33:25.019329380Z {"level":"info","db_version":"42","code_version":"42","time":"2026-11-03T12:33:25Z","message":"Version comparison"}
api-1 | 2026-11-03T12:33:25.019340672Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Database is up to date, no migrations needed"}
api-1 | 2026-11-03T12:33:25.019342630Z {"level":"info","max_connections":100,"max_connections_per_db":3,"time":"2026-11-03T12:33:25Z","message":"Connection manager initialized"}
api-1 | 2026-11-03T12:33:25.020170964Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"System installation verified"}
api-1 | 2026-11-03T12:33:25.020180589Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Blog cache initialized"}
api-1 | 2026-11-03T12:33:25.020187130Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Using SMTP mailer for production"}
api-1 | 2026-11-03T12:33:25.021148130Z {"level":"info","state":"none","tier":"community","max_workspaces":"3","sso_gated":false,"audit_logs_recording":false,"key_source":"none","org":"","expires_at":"n/a","features":"","time":"2026-11-03T12:33:25Z","message":"Licence resolved"}
api-1 | 2026-11-03T12:33:25.021156880Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Annotation service registered with event bus"}
api-1 | 2026-11-03T12:33:25.021158089Z {"level":"info","task_type":"send_broadcast","time":"2026-11-03T12:33:25Z","message":"Registered task processor"}
api-1 | 2026-11-03T12:33:25.021158964Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Broadcast orchestrator registered with task service"}
api-1 | 2026-11-03T12:33:25.021159630Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"TaskService subscribed to broadcast events"}
api-1 | 2026-11-03T12:33:25.021160380Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"System notification service registered with event bus"}
api-1 | 2026-11-03T12:33:25.021161005Z {"level":"info","task_type":"build_segment","time":"2026-11-03T12:33:25Z","message":"Registered task processor"}
api-1 | 2026-11-03T12:33:25.021161672Z {"level":"info","task_type":"check_segment_recompute","time":"2026-11-03T12:33:25Z","message":"Registered task processor"}
api-1 | 2026-11-03T12:33:25.021162380Z {"level":"info","task_type":"process_contact_segment_queue","time":"2026-11-03T12:33:25Z","message":"Registered task processor"}
api-1 | 2026-11-03T12:33:25.021470422Z {"level":"info","task_type":"sync_integration","time":"2026-11-03T12:33:25Z","message":"Registered task processor"}
api-1 | 2026-11-03T12:33:25.021471339Z {"level":"info","task_type":"web_analytics_backfill","time":"2026-11-03T12:33:25Z","message":"Registered task processor"}
api-1 | 2026-11-03T12:33:25.021472172Z {"level":"info","path":"/app/geoip/GeoLite2-City.mmdb","time":"2026-11-03T12:33:25Z","message":"GeoIP database loaded"}
api-1 | 2026-11-03T12:33:25.022817630Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Application successfully initialized"}
api-1 | 2026-11-03T12:33:25.023286547Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Server started successfully"}
api-1 | 2026-11-03T12:33:25.023291255Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Graceful shutdown middleware enabled"}
api-1 | 2026-11-03T12:33:25.023292172Z {"level":"info","address":"0.0.0.0:8080","api_endpoint":"http://localhost:8080","port":8080,"time":"2026-11-03T12:33:25Z","message":"Server starting on 0.0.0.0:8080 with API endpoint: http://localhost:8080"}
api-1 | 2026-11-03T12:33:25.023293214Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Web analytics maintenance worker started"}
api-1 | 2026-11-03T12:33:25.023294047Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Webhook delivery worker will start in 30 seconds..."}
api-1 | 2026-11-03T12:33:25.023294672Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Task scheduler will start in 30 seconds..."}
api-1 | 2026-11-03T12:33:25.023300172Z {"level":"info","time":"2026-11-03T12:33:25Z","message":"Email queue worker will start in 30 seconds..."}
api-1 | 2026-11-03T12:33:25.023300880Z {"level":"info","delay":30000000000,"time":"2026-11-03T12:33:25Z","message":"Automation scheduler will start after delay..."}
api-1 | 2026-11-03T12:33:55.024816338Z {"level":"info","time":"2026-11-03T12:33:55Z","message":"Starting automation scheduler now"}
api-1 | 2026-11-03T12:33:55.024842921Z {"level":"info","interval":10000000000,"batch_size":50,"time":"2026-11-03T12:33:55Z","message":"Starting automation scheduler"}
api-1 | 2026-11-03T12:33:55.024844546Z {"level":"info","time":"2026-11-03T12:33:55Z","message":"Starting webhook delivery worker now"}
api-1 | 2026-11-03T12:33:55.024848504Z {"level":"info","time":"2026-11-03T12:33:55Z","message":"Webhook delivery worker started"}
api-1 | 2026-11-03T12:33:55.024849504Z {"level":"info","time":"2026-11-03T12:33:55Z","message":"Starting email queue worker now"}
api-1 | 2026-11-03T12:33:55.024850296Z {"level":"info","worker_count":5,"poll_interval":"1s","batch_size":200,"send_concurrency":10,"smtp_send_concurrency":1,"round_window":"2s","time":"2026-11-03T12:33:55Z","message":"Starting email queue worker"}
api-1 | 2026-11-03T12:33:55.024915713Z {"level":"info","time":"2026-11-03T12:33:55Z","message":"Starting task scheduler now"}
api-1 | 2026-11-03T12:33:55.024964421Z {"level":"info","interval":20000000000,"max_tasks":100,"time":"2026-11-03T12:33:55Z","message":"Starting internal task scheduler"}
api-1 | 2026-11-03T12:33:55.028535004Z {"level":"info","task_count":0,"time":"2026-11-03T12:33:55Z","message":"Retrieved batch of tasks to process"}
api-1 | 2026-11-03T12:34:15.033428556Z {"level":"info","task_count":0,"time":"2026-11-03T12:34:15Z","message":"Retrieved batch of tasks to process"}
api-1 | 2026-11-03T12:34:35.031041258Z {"level":"info","task_count":0,"time":"2026-11-03T12:34:35Z","message":"Retrieved batch of tasks to process"}
api-1 | 2026-11-03T12:34:55.031237669Z {"level":"info","task_count":0,"time":"2026-11-03T12:34:55Z","message":"Retrieved batch of tasks to process"}
api-1 | 2026-11-03T12:35:15.034844762Z {"level":"info","task_count":0,"time":"2026-11-03T12:35:15Z","message":"Retrieved batch of tasks to process"}
api-1 | 2026-11-03T12:35:35.031794173Z {"level":"info","task_count":0,"time":"2026-11-03T12:35:35Z","message":"Retrieved batch of tasks to process"}
api-1 | 2026-11-03T12:35:55.032229626Z {"level":"info","task_count":0,"time":"2026-11-03T12:35:55Z","message":"Retrieved batch of tasks to process"}
api-1 | 2026-11-03T12:36:15.028866385Z {"level":"info","task_count":0,"time":"2026-11-03T12:36:15Z","message":"Retrieved batch of tasks to process"}
postgres-1 | 2026-11-03T12:33:18.564262377Z The default text search configuration will be set to "english".
postgres-1 | 2026-11-03T12:33:18.564263002Z
postgres-1 | 2026-11-03T12:33:18.564263502Z Data page checksums are disabled.
postgres-1 | 2026-11-03T12:33:18.564264044Z
postgres-1 | 2026-11-03T12:33:18.564264544Z fixing permissions on existing directory /var/lib/postgresql/data ... ok
postgres-1 | 2026-11-03T12:33:18.566022086Z creating subdirectories ... ok
postgres-1 | 2026-11-03T12:33:18.566030711Z selecting dynamic shared memory implementation ... posix
postgres-1 | 2026-11-03T12:33:18.573756252Z selecting default "max_connections" ... 100
postgres-1 | 2026-11-03T12:33:18.582473877Z selecting default "shared_buffers" ... 128MB
postgres-1 | 2026-11-03T12:33:18.593965002Z selecting default time zone ... UTC
postgres-1 | 2026-11-03T12:33:18.594218752Z creating configuration files ... ok
postgres-1 | 2026-11-03T12:33:18.641524919Z running bootstrap script ... ok
postgres-1 | 2026-11-03T12:33:18.704688377Z sh: locale: not found
postgres-1 | 2026-11-03T12:33:18.704708044Z 2026-11-03 12:33:18.704 UTC [34] WARNING: no usable system locales were found
postgres-1 | 2026-11-03T12:33:18.842880502Z performing post-bootstrap initialization ... ok
postgres-1 | 2026-11-03T12:33:18.889059127Z syncing data to disk ... ok
postgres-1 | 2026-11-03T12:33:18.889082961Z
postgres-1 | 2026-11-03T12:33:18.889084586Z
postgres-1 | 2026-11-03T12:33:18.889085586Z Success. You can now start the database server using:
postgres-1 | 2026-11-03T12:33:18.889086169Z
postgres-1 | 2026-11-03T12:33:18.889087627Z pg_ctl -D /var/lib/postgresql/data -l logfile start
postgres-1 | 2026-11-03T12:33:18.889088419Z
postgres-1 | 2026-11-03T12:33:18.889130377Z initdb: warning: enabling "trust" authentication for local connections
postgres-1 | 2026-11-03T12:33:18.889132586Z initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.
postgres-1 | 2026-11-03T12:33:18.911125836Z waiting for server to start....2026-11-03 12:33:18.910 UTC [40] LOG: starting PostgreSQL 17.11 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
postgres-1 | 2026-11-03T12:33:18.912077377Z 2026-11-03 12:33:18.912 UTC [40] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres-1 | 2026-11-03T12:33:18.913612586Z 2026-11-03 12:33:18.913 UTC [43] LOG: database system was shut down at 2026-11-03 12:33:18 UTC
postgres-1 | 2026-11-03T12:33:18.915686086Z 2026-11-03 12:33:18.915 UTC [40] LOG: database system is ready to accept connections
postgres-1 | 2026-11-03T12:33:19.000745919Z done
postgres-1 | 2026-11-03T12:33:19.000760044Z server started
postgres-1 | 2026-11-03T12:33:19.006779377Z
postgres-1 | 2026-11-03T12:33:19.006823752Z /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
postgres-1 | 2026-11-03T12:33:19.006842169Z
postgres-1 | 2026-11-03T12:33:19.007126627Z 2026-11-03 12:33:19.007 UTC [40] LOG: received fast shutdown request
postgres-1 | 2026-11-03T12:33:19.008474294Z waiting for server to shut down....2026-11-03 12:33:19.007 UTC [40] LOG: aborting any active transactions
postgres-1 | 2026-11-03T12:33:19.009582211Z 2026-11-03 12:33:19.009 UTC [40] LOG: background worker "logical replication launcher" (PID 46) exited with exit code 1
postgres-1 | 2026-11-03T12:33:19.009594252Z 2026-11-03 12:33:19.009 UTC [41] LOG: shutting down
postgres-1 | 2026-11-03T12:33:19.010416127Z 2026-11-03 12:33:19.010 UTC [41] LOG: checkpoint starting: shutdown immediate
postgres-1 | 2026-11-03T12:33:19.012911211Z 2026-11-03 12:33:19.012 UTC [41] LOG: checkpoint complete: wrote 3 buffers (0.0%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.002 s, sync=0.001 s, total=0.004 s; sync files=2, longest=0.001 s, average=0.001 s; distance=0 kB, estimate=0 kB; lsn=0/14F3F88, redo lsn=0/14F3F88
postgres-1 | 2026-11-03T12:33:19.013924377Z 2026-11-03 12:33:19.013 UTC [40] LOG: database system is shut down
postgres-1 | 2026-11-03T12:33:19.111139628Z done
postgres-1 | 2026-11-03T12:33:19.111156169Z server stopped
postgres-1 | 2026-11-03T12:33:19.111511628Z
postgres-1 | 2026-11-03T12:33:19.111515586Z PostgreSQL init process complete; ready for start up.
postgres-1 | 2026-11-03T12:33:19.111516419Z
postgres-1 | 2026-11-03T12:33:19.119518461Z 2026-11-03 12:33:19.119 UTC [1] LOG: starting PostgreSQL 17.11 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
postgres-1 | 2026-11-03T12:33:19.119551586Z 2026-11-03 12:33:19.119 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
postgres-1 | 2026-11-03T12:33:19.119553711Z 2026-11-03 12:33:19.119 UTC [1] LOG: listening on IPv6 address "::", port 5432
postgres-1 | 2026-11-03T12:33:19.120866919Z 2026-11-03 12:33:19.120 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres-1 | 2026-11-03T12:33:19.122144794Z 2026-11-03 12:33:19.122 UTC [54] LOG: database system was shut down at 2026-11-03 12:33:19 UTC
postgres-1 | 2026-11-03T12:33:19.124034294Z 2026-11-03 12:33:19.123 UTC [1] LOG: database system is ready to accept connections
postgres-1 | 2026-11-03T12:33:24.158797547Z 2026-11-03 12:33:24.157 UTC [65] FATAL: database "notifuse_system" does not exist