A lightweight, decoupled notification and email dispatch daemon. While originally engineered as an out-of-process companion for the hardened wordpress-docker production stack, go-notifier is completely application-agnostic and serves as a secure notification sidecar for any web application or microservice (PHP, Python, Node.js, Go, Rust, Ruby).
The service runs as a zero-dependency, non-root scratch container, isolating mail transport credentials and multi-channel alert dispatch logic behind a local Unix domain socket.
Hardened production environments (such as the wordpress-docker runtime) enforce strict zero-trust containment: immutable root filesystems (read_only: true), dropped Linux capabilities (cap_drop: [ALL]), shell-less container runtimes (complete removal of shell binaries), and the total elimination of local Mail Transfer Agents (Sendmail, Exim, Postfix).
Traditional web application email architectures introduce severe security liabilities:
- In-Container MTAs: Rely on setuid binaries and shell execution (
/usr/sbin/sendmail). In the event of an application-layer Remote Code Execution (RCE) flaw, attackers routinely weaponize local MTA binaries to break containment or spawn interactive shells. - Direct Outbound SMTP from App Workers: Storing SMTP credentials in application memory or environment exposes them to memory disclosure vulnerabilities, while opening outbound network sockets directly from worker threads expands container attack surface and blocks execution during external TLS handshakes.
Architecture
go-notifier decouples notification and mail transport into an isolated, unprivileged scratch sidecar:
[ Application (PHP / Python / Node / Go) ]
│
│ HTTP / JSON over Unix Domain Socket
▼ (/var/run/sockets/notify/notify.sock - mode 0600)
[ go-notifier (scratch daemon) ]
├── Rate Limiting (Token Bucket)
├── Recipient Filter (admin_emails check)
└── Routing Engine (Subject Regex)
├── SMTP Relay (Port 465 / 587 TLS)
├── Telegram Bot API (HTTPS)
├── Matrix Homeserver API (HTTPS)
└── ntfy Endpoint (HTTPS)
- Shell-less Scratch Container: Statically compiled Go binary (
CGO_ENABLED=0) running in an emptyscratchimage with trusted CA certificates. No shell binaries, no package manager, zero local attack surface. - Pure UNIX Domain Socket IPC: Zero exposed TCP network ports to Docker bridge or host. Communication occurs strictly over an in-memory Unix domain socket (
notify.sock) shared via tmpfs with0600permissions restricted to matching UID/GID (APP_UID:APP_GID). - Zero-Privilege Security Profile: Immutable root filesystem (
read_only: true), all Linux capabilities dropped (cap_drop: [ALL]), no capabilities added (cap_add: []), privilege escalation blocked (no-new-privileges: true), and strict resource limits (64 MB RAM, 0.5 CPU, 30 PIDs). - Strict Docker Secrets: SMTP relay credentials and messenger API tokens are mounted exclusively from disk secret files (
0400). Zero sensitive credentials exist in container environment variables or command-line arguments. - Granular Multi-Channel Dispatch: Intelligently routes notifications across SMTP, Telegram, Matrix, and ntfy using subject regular expression matching, administrator recipient filtering (
admin_emails.txt), and per-rule attachment forwarding policies. - Startup Validation: Validates channel credentials and routing rules on boot. If an active rule or default target references an unconfigured channel or disabled SMTP, the daemon terminates immediately with a descriptive error.
Although designed for the wordpress-docker runtime, go-notifier contains no WordPress-specific dependencies, protocols, or logic.
Any software stack capable of dispatching an HTTP POST request over a local Unix socket can leverage go-notifier:
- Modern Frameworks & Backends: Laravel, Symfony, FastAPI, Django, Flask, Express, NestJS, Ruby on Rails, Go, or Rust services.
- Zero-MTA / Distroless Environments: Ideal for distroless or minimal Alpine/Debian images where installing full MTA suites is prohibited by security policy.
- Microservices & Kubernetes Sidecars: Functions as a shared sidecar container within a Kubernetes Pod or Docker Compose project, abstracting TLS handshakes, rate limiting, and push messenger routing away from core application code.
Supported Channels & Protocols
| Channel | Protocol | Transport Security | Payload Support |
|---|---|---|---|
| SMTP | SMTP / ESMTP | Implicit TLS (Port 465) or STARTTLS (Port 587) | Text, HTML, MIME Attachments |
| Telegram | HTTP POST | TLS 1.3 (api.telegram.org) |
Markdown Text (auto-truncated at 3500 chars), Document Attachments |
| Matrix | Client-Server API v3 | TLS 1.3 (Direct Homeserver endpoint) | Plain / HTML Text, m.file Events |
| ntfy | HTTP POST | TLS 1.3 (Self-hosted or ntfy.sh) |
Plain Text, Binary Attachments |
API Specification
The daemon listens on the configured Unix domain socket (/var/run/sockets/notify/notify.sock).
Dispatches an outbound notification or email message.
Content-Type: application/json{
"subject": "Critical Security Alert: Brute-force detected",
"body_text": "IP 192.0.2.1 blocked after 5 failed attempts.",
"body_html": "<p>IP <code>192.0.2.1</code> blocked after 5 failed attempts.</p>",
"reply_to": "security@example.com",
"to": [
"admin@example.com"
],
"attachments": [
{
"filename": "audit-report.pdf",
"mime_type": "application/pdf",
"content_base64": "JVBERi0xLjQKJ..."
}
]
}subject(string, optional): Message subject used for mail headers and regex route evaluation. At least one ofsubject,body_text, orbody_htmlis required.body_text(string, optional): Plain-text fallback content.body_html(string, optional): HTML-formatted content.reply_to(string, optional): Email address forReply-Toheader.to(array of strings, optional): Destination email addresses. Mandatory for SMTP delivery; optional for messenger-only notifications.attachments(array of objects, optional): File attachments encoded in Base64.
200 OK:{"status":"delivered"}— Payload successfully dispatched to all active channels.400 Bad Request:{"error":"..."}— Malformed JSON, completely empty notification (neither subject nor body provided), or missing recipients (to) when routed exclusively to SMTP.413 Payload Too Large:{"error":"..."}— Attachment size exceedsNOTIFY_MAX_ATTACHMENT_SIZE_MB.429 Too Many Requests:{"error":"rate limit exceeded"}— Dispatch frequency throttled by token bucket.502 Bad Gateway:{"error":"dispatch failed: ..."}— Relay failure reported by upstream transport.
curl --unix-socket /var/run/sockets/notify/notify.sock http://_/notify \
-H "Content-Type: application/json" \
-d '{
"subject": "System Security Alert",
"to": ["admin@example.com"],
"body_text": "Failed authentication threshold exceeded."
}'Note on URL Syntax: The host
_inhttp://_/notifyis a dummy placeholder required bycurlto construct a valid HTTP/1.1 request line andHost:header. Because--unix-socketroutes all traffic directly to the local socket file, no DNS lookup or TCP networking occurs. Do not replace_with a real hostname or IP address.
Liveness probe endpoint.
curl --unix-socket /var/run/sockets/notify/notify.sock http://_/healthz{"status":"ok"}Configuration Reference
Configuration is supplied via environment variables (operational knobs) and secret files (credentials).
| Variable | Default | Description |
|---|---|---|
SITE_NAME |
WordPress Site |
Site identifier prefixed to notifications (e.g. [SiteName] Subject). Fallback: SITE_USER -> SERVER_NAME. |
SMTP_FROM_NAME |
None | Optional sender display name formatted into outbound email From header (e.g. "Display Name" <noreply@example.com>). |
NOTIFY_SMTP_ENABLED |
true |
When true, SMTP relay is enabled and mandatory (requires SMTP secrets). When false, SMTP is completely disabled and secrets are ignored. |
NOTIFY_ADMIN_FILTER_REQUIRED |
true |
Evaluated only when NOTIFY_SMTP_ENABLED=true. When true, messenger delivery requires recipient matching in admin_emails.txt. When false, recipient filtering is bypassed. Ignored when NOTIFY_SMTP_ENABLED=false. |
NOTIFY_CHANNELS |
Auto-detected | Comma-separated default channels for admin alerts (smtp,telegram,matrix,ntfy). If unset, automatically aggregates all enabled channels whose credentials are present. Explicitly requesting an unconfigured channel causes a fail-fast validation error at startup. |
NOTIFY_RATE_LIMIT_PER_MINUTE |
10 |
Token bucket refill rate. Set to 0 to disable throttling. |
NOTIFY_BURST |
5 |
Maximum token bucket burst capacity. |
NOTIFY_MAX_ATTACHMENT_SIZE_MB |
10 |
Maximum size per individual attachment in megabytes. Set to 0 to disable limit. |
The interaction between NOTIFY_SMTP_ENABLED and NOTIFY_ADMIN_FILTER_REQUIRED defines three distinct operational modes:
| Mode | NOTIFY_SMTP_ENABLED |
NOTIFY_ADMIN_FILTER_REQUIRED |
Operational Behavior |
|---|---|---|---|
| Strict Isolation (Default) | true |
true |
SMTP is mandatory. Non-admin recipients route strictly to SMTP. Messenger alerts require recipient match in admin_emails.txt; if empty, messengers receive nothing. |
| Open Multi-Channel | true |
false |
SMTP is mandatory. Recipient filtering is disabled. All messages route to matched rule targets or NOTIFY_CHANNELS (both SMTP and messengers). |
| Push-Only Sidecar (No SMTP) | false |
* (Ignored) |
SMTP is completely disabled; all SMTP secrets and channels are ignored. Messages dispatch strictly to messengers (Telegram, Matrix, ntfy). Recipient filtering is bypassed. |
Granular Routing Engine
Administrative alerts can be selectively dispatched to distinct channels and granted attachment permissions based on subject regular expressions:
NOTIFY_RULE_<NAME>_MATCH="<regex>"
NOTIFY_RULE_<NAME>_TARGETS="<channel1,channel2>"
NOTIFY_RULE_<NAME>_ATTACHMENTS="true|false"- Recipient Filtering: When both
NOTIFY_SMTP_ENABLED=trueandNOTIFY_ADMIN_FILTER_REQUIRED=true, messages whose recipients do not matchadmin_emails.txtare relayed exclusively tosmtp. When filtering is disabled (false) or SMTP is disabled, this check is bypassed. - Subject Matching: For eligible messages, rules are scanned against
subject.- On match: Message is dispatched to channels specified in
NOTIFY_RULE_<NAME>_TARGETS(orNOTIFY_CHANNELSif targets are omitted). Attachments are forwarded only ifNOTIFY_RULE_<NAME>_ATTACHMENTS=true.
- On match: Message is dispatched to channels specified in
- Default Fallback: If an eligible message matches no custom rules, it is routed to
NOTIFY_CHANNELSwith attachments disabled (false). - Multi-Target Resilience: If a message has no
torecipients and targets bothsmtpand messengers,smtpdelivery is skipped without failing the request, and the alert is successfully delivered to all active push messengers. Only when a message is routed exclusively tosmtpwithout recipients will it return400 Bad Request.
The daemon validates all configured channels and routing targets during initialization:
- If a channel specified in
NOTIFY_CHANNELSor any routing rule (NOTIFY_RULE_<NAME>_TARGETS) lacks required credentials, or ifsmtpis requested whileNOTIFY_SMTP_ENABLED=false, the daemon terminates startup with a validation error. - If SMTP is disabled (
NOTIFY_SMTP_ENABLED=false) and no messenger secrets are configured, the daemon terminates startup immediately (no notification channels configured).
# Route security and intrusion alerts (Wordfence) to Matrix and SMTP without attachments
NOTIFY_RULE_WORDFENCE_MATCH="(?i)wordfence|security alert|attack"
NOTIFY_RULE_WORDFENCE_TARGETS="smtp,matrix"
NOTIFY_RULE_WORDFENCE_ATTACHMENTS=false
# Route job applications and contact forms to ntfy and SMTP with attachments
NOTIFY_RULE_FORMS_MATCH="(?i)contact form|job application|resume"
NOTIFY_RULE_FORMS_TARGETS="smtp,ntfy"
NOTIFY_RULE_FORMS_ATTACHMENTS=true
# Route store orders to Telegram and SMTP without attachments
NOTIFY_RULE_ORDERS_MATCH="(?i)new order|order #[0-9]+"
NOTIFY_RULE_ORDERS_TARGETS="smtp,telegram"
NOTIFY_RULE_ORDERS_ATTACHMENTS=falseDeployment & Setup
├── docker-compose.yaml # Production deployment manifest
├── .env # Host infrastructure variables (UID, GID, site user)
├── notifier.env # Optional: channels, routing rules, and limits
├── secrets/ # Docker secrets directory (owner-only access, mode 0700)
│ ├── smtp_host.txt # Mandatory if NOTIFY_SMTP_ENABLED=true: relay hostname or IP
│ ├── smtp_port.txt # Mandatory if NOTIFY_SMTP_ENABLED=true: port (465 or 587)
│ ├── smtp_mail.txt # Mandatory if NOTIFY_SMTP_ENABLED=true: sender email / username
│ ├── smtp_password.txt # Mandatory if NOTIFY_SMTP_ENABLED=true: authentication password
│ ├── admin_emails.txt # Required for messengers when SMTP & admin filter are enabled (default)
│ ├── telegram_bot_token.txt # Optional: Telegram Bot API token
│ ├── telegram_chat_id.txt # Optional: Telegram target chat ID
│ ├── matrix_url.txt # Optional: Matrix homeserver URL
│ ├── matrix_room_id.txt # Optional: Matrix internal room ID
│ ├── matrix_access_token.txt # Optional: Matrix bot access token
│ ├── ntfy_url.txt # Optional: ntfy server URL
│ ├── ntfy_topic.txt # Optional: ntfy topic name
│ └── ntfy_token.txt # Optional: ntfy Bearer authentication token
- Docker Engine & Compose: Ensure Docker Engine and Docker Compose plugin are installed on the host. Follow the official installation guide for Ubuntu.
For multi-tenant isolation and security compliance, run go-notifier under a dedicated unprivileged user (matching your web application worker):
SITE_USER=mysite
sudo useradd -m -d /home/${SITE_USER} -s /usr/sbin/nologin ${SITE_USER}Identify the UID and GID to bind container execution and socket file ownership:
APP_UID=$(id -u ${SITE_USER})
APP_GID=$(id -g ${SITE_USER})(If deploying within an existing application directory without creating a new user, inspect your current user: SITE_USER=$(whoami); APP_UID=$(id -u); APP_GID=$(id -g))
Create the secrets directory with owner-only access:
sudo -u ${SITE_USER} mkdir -p /home/${SITE_USER}/secrets
sudo chmod 0700 /home/${SITE_USER}/secretsCreate the secret files for your configured channels:
SMTP:
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/smtp_host.txt
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/smtp_port.txt
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/smtp_mail.txt
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/smtp_password.txtTelegram:
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/telegram_bot_token.txt
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/telegram_chat_id.txtMatrix:
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/matrix_url.txt
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/matrix_room_id.txt
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/matrix_access_token.txtntfy:
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/ntfy_url.txt
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/ntfy_topic.txt
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/ntfy_token.txtAdmin recipient whitelist (admin_emails.txt):
sudo -u ${SITE_USER} nano /home/${SITE_USER}/secrets/admin_emails.txtLock secret files with strict read-only permissions:
sudo chmod 0400 /home/${SITE_USER}/secrets/*.txtDownload configuration templates directly from the repository using curl:
REPO="https://raw.githubusercontent.com/webstudiobond/go-notifier/main"
sudo -u ${SITE_USER} curl -fsSL ${REPO}/docker-compose.yaml -o /home/${SITE_USER}/docker-compose.yaml
sudo -u ${SITE_USER} curl -fsSL ${REPO}/example.env -o /home/${SITE_USER}/.env
sudo -u ${SITE_USER} curl -fsSL ${REPO}/notifier.env.example -o /home/${SITE_USER}/notifier.envEdit .env to supply SITE_USER, APP_UID, and APP_GID identified in Step 1:
sudo -u ${SITE_USER} nano /home/${SITE_USER}/.envSee example.env for all available variables.
Customize active channels, rate limits, and regex routing rules in notifier.env:
sudo -u ${SITE_USER} nano /home/${SITE_USER}/notifier.envSee notifier.env.example for all available options, channels, and regex routing rules.
Enforce strict ownership and access rights across the deployment directory:
sudo chown -R ${SITE_USER}:${SITE_USER} /home/${SITE_USER}
sudo chmod 0700 /home/${SITE_USER}/secrets
sudo chmod 0400 /home/${SITE_USER}/secrets/*.txt
sudo chmod 0600 /home/${SITE_USER}/.env
[ -f /home/${SITE_USER}/notifier.env ] && sudo chmod 0600 /home/${SITE_USER}/notifier.envPull the scratch image and start the service:
docker compose -f /home/${SITE_USER}/docker-compose.yaml pull
docker compose -f /home/${SITE_USER}/docker-compose.yaml up -dFollow daemon logs to confirm channel initialization:
docker compose -f /home/${SITE_USER}/docker-compose.yaml logs -f notifierVerify service liveness via the Unix domain socket:
curl --unix-socket /var/run/sockets/notify/notify.sock http://_/healthzTo stop the service:
docker compose -f /home/${SITE_USER}/docker-compose.yaml downFor microservices and environments requiring only push alerts (Telegram, Matrix, ntfy) without SMTP relay infrastructure:
- In
notifier.env, setNOTIFY_SMTP_ENABLED=false. - In
docker-compose.yaml, comment out the 4smtp_*secret entries undersecrets:. - Provide required messenger secrets in
secrets/.
Development
For instructions on building from source, running the test suite, and local verification gates, refer to DEVELOPMENT.