ulaknode FAQs

Frequently Asked Questions

ulaknode

  • 🔗 What is ulaknode and how does it differ from hosted email services?

    ulaknode is a free, self-hosted email server for Linux distributed as a single Docker container. Rather than one large all-in-one filter, it bundles a set of focused, industry-standard components into one cohesive package:

    • Postfix — SMTP mail transfer
    • Dovecot — IMAP mailbox access
    • OpenDKIM & OpenDMARC — DKIM signing/verification and DMARC enforcement
    • policyd-spf — SPF checking
    • Postgrey — greylisting
    • SpamAssassin — content-based spam scoring
    • ClamAV — antivirus scanning of attachments
    • Postscreen & Unbound — DNSBL blocklist checking (Spamhaus, SpamCop) via a dedicated recursive resolver
    • Fail2ban — bans brute-force SMTP/IMAP clients at the application layer

    Unlike hosted services such as Google Workspace or Microsoft 365, ulaknode gives you complete control over your data. There are no per-user or per-mailbox fees, and you are not subject to a third-party provider's terms of service, storage limits, or data-residency policies. The trade-off is that you are responsible for operating the Linux host it runs on.

  • 🔗 What are the system requirements for running ulaknode?

    ulaknode requires a Linux server (physical, VM, or VPS) with Docker installed. Minimum recommended specifications for a small organisation:

    • CPU: 2 vCPUs
    • RAM: 4 GB minimum — clamd alone can use up to ~2 GB RSS once its virus signature database is loaded
    • Disk: 40 GB for the OS and container image, plus mailbox storage as needed
    • Network: A static public IP address and a fully-qualified domain name (FQDN) with a matching reverse DNS (PTR) record

    Port 25 (SMTP) must be open outbound. Many cloud providers block port 25 by default — check with your hosting provider before deploying.

    Outbound UDP/TCP port 53 must also be open to the internet at large (not just to one resolver). The container runs its own recursive DNS resolver (Unbound) for DNSBL blocklist checks, which queries root/TLD/authoritative servers directly rather than forwarding to your host's resolver — some providers restrict outbound DNS to their own resolver only, which would need an exception.

    If you cap the container's memory (docker run -m, mem_limit, or a Compose deploy.resources.limits.memory), keep it at 3 GB or more. A tighter limit gets clamd silently killed by the cgroup OOM killer, disabling virus scanning while the rest of the stack keeps running with no obvious error pointing at the cause.

  • 🔗 How do I start ulaknode?

    ulaknode is a single-container image, so no compose file is needed — replace mail.yourdomain.com with your actual mail hostname and run:

    docker run -d \
      --name ulaknode \
      --hostname mail.yourdomain.com \
      --restart unless-stopped \
      -e MAIL_HOSTNAME=mail.yourdomain.com \
      -p 25:25 -p 587:587 -p 993:993 \
      -v ulak_mail:/var/mail \
      -v ulak_logs:/var/log \
      -v ulak_clamav:/var/lib/clamav \
      -v ulak_postfix:/etc/postfix \
      -v ulak_dovecot:/etc/dovecot \
      -v ulak_opendkim:/etc/opendkim \
      -v ulak_opendmarc:/etc/opendmarc \
      -v ulak_spamassassin:/etc/spamassassin \
      -v ulak_policyd_spf:/etc/postfix-policyd-spf-python \
      -v ulak_postgrey:/etc/postgrey \
      -v ulak_fail2ban:/etc/fail2ban \
      -v ulak_unbound:/etc/unbound \
      -v ulak_certs:/etc/ssl/mail \
      -v /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem:/run/certs/fullchain.pem:ro \
      -v /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem:/run/certs/privkey.pem:ro \
      itefixnet/ulaknode:latest

    Named volumes are created automatically on first run. On first boot the container seeds all configuration volumes from the image defaults and applies your hostname automatically. Services start in dependency order: Unbound → ClamAV → Postgrey → OpenDKIM → OpenDMARC → SpamAssassin → Postfix → Dovecot → Fail2ban.

    ClamAV loads its full signature database (~1 GB) during startup. Wait about a minute before testing mail flow after a fresh start or restart.

    To check the version of all bundled components:

    docker exec ulaknode ulaknode-version
  • 🔗 What DNS records do I need to set up before going live?

    Correct DNS configuration is essential for mail deliverability. You need at minimum:

    • MX record — points your domain to your mail server hostname, e.g. mail.yourdomain.com
    • A record — resolves mail.yourdomain.com to your server's public IP
    • PTR record (reverse DNS) — resolves your public IP back to mail.yourdomain.com; configured with your hosting or ISP provider
    • SPF TXT record — use ulaknode-spf generate to produce the correct value (see the SPF FAQ below)
    • DKIM TXT record — use ulaknode-dkim show to retrieve the value to publish (see the DKIM FAQ below)
    • DMARC TXT record — use ulaknode-dmarc generate to produce the correct value (see the DMARC FAQ below)

    Missing or incorrect PTR and SPF records are the most common reason legitimate mail is rejected by major providers. Verify all records with a tool such as MXToolbox after deployment.

  • 🔗 How do I configure DKIM signing?

    ulaknode includes the ulaknode-dkim tool that manages DKIM keys and updates OpenDKIM's signing configuration automatically.

    1. Generate a 2048-bit RSA key pair for your domain:

    docker exec ulaknode ulaknode-dkim generate yourdomain.com

    The default selector is mail. You can pass a custom selector as a second argument.

    2. Retrieve the DNS TXT record to publish:

    docker exec ulaknode ulaknode-dkim show yourdomain.com

    The output shows the exact TXT record to add to your DNS zone, in the form:

    mail._domainkey.yourdomain.com  IN TXT  "v=DKIM1; k=rsa; p=<public-key>"

    3. Reload OpenDKIM to apply the new key:

    docker exec ulaknode ulaknode-service reload opendkim

    Other useful commands:

    # List all DKIM keys
    docker exec ulaknode ulaknode-dkim list
    
    # Remove a key
    docker exec ulaknode ulaknode-dkim remove yourdomain.com mail

    After DNS propagation, verify signing with mail-tester.com — look for DKIM=pass in the received headers.

  • 🔗 How do I generate an SPF record?

    The ulaknode-spf tool generates a ready-to-publish SPF TXT record and can also look up what is currently published in DNS.

    Basic SPF using the domain's own MX hosts (recommended starting point):

    docker exec ulaknode ulaknode-spf generate yourdomain.com --mx

    Add a specific IP address and use a strict fail policy:

    docker exec ulaknode ulaknode-spf generate yourdomain.com --mx --ip4 203.0.113.10 --all fail

    Authorize an external sending service:

    docker exec ulaknode ulaknode-spf generate yourdomain.com --mx --include sendgrid.net --all softfail

    Available --all policies:

    • softfail (~all) — unauthorized senders are flagged but not rejected; recommended when starting out
    • fail (-all) — unauthorized senders are rejected outright
    • neutral (?all) — no assertion about unauthorized senders

    Look up the currently published SPF record:

    docker exec ulaknode ulaknode-spf show yourdomain.com

    Copy the output into a DNS TXT record for yourdomain.com.

  • 🔗 How do I set up a DMARC policy?

    The ulaknode-dmarc tool generates a ready-to-publish DMARC TXT record.

    Start in monitoring mode (no action on failures, collect aggregate reports):

    docker exec ulaknode ulaknode-dmarc generate yourdomain.com \
      --policy none \
      --rua mailto:dmarc@yourdomain.com

    Move to quarantine once you are confident in DKIM and SPF:

    docker exec ulaknode ulaknode-dmarc generate yourdomain.com \
      --policy quarantine \
      --rua mailto:dmarc@yourdomain.com

    Strict reject mode:

    docker exec ulaknode ulaknode-dmarc generate yourdomain.com \
      --policy reject \
      --rua mailto:dmarc@yourdomain.com \
      --pct 100

    The output is the exact TXT record value to publish under _dmarc.yourdomain.com.

    Look up the currently published DMARC record:

    docker exec ulaknode ulaknode-dmarc show yourdomain.com

    The recommended rollout sequence is: nonequarantinereject, advancing only after reviewing aggregate reports to ensure no legitimate mail is failing.

  • 🔗 How do I add or remove email domains?

    Use the ulaknode-domain tool to manage virtual domains hosted on your server.

    # Add a domain
    docker exec ulaknode ulaknode-domain add yourdomain.com
    
    # List all hosted domains
    docker exec ulaknode ulaknode-domain list
    
    # Show all mailboxes and aliases for a domain
    docker exec ulaknode ulaknode-domain show yourdomain.com
    
    # Remove a domain and all its associated mailboxes and aliases
    docker exec ulaknode ulaknode-domain remove yourdomain.com

    After adding a domain, set up the DNS records described in the DNS FAQ above, then create mailboxes and DKIM keys for the domain.

  • 🔗 How do I add, remove, or manage email accounts?

    Use the ulaknode-mailbox tool for all mailbox operations.

    Create a new mailbox:

    docker exec ulaknode ulaknode-mailbox add alice@yourdomain.com secretpassword

    List all mailboxes for a domain:

    docker exec ulaknode ulaknode-mailbox list yourdomain.com

    Change a mailbox password:

    docker exec ulaknode ulaknode-mailbox passwd alice@yourdomain.com newpassword

    Set a storage quota:

    docker exec ulaknode ulaknode-mailbox quota-set alice@yourdomain.com 5G

    Accepted suffixes: K, M, G. Use 0 for unlimited (the default).

    Check current quota usage:

    docker exec ulaknode ulaknode-mailbox quota alice@yourdomain.com

    Show mailbox details:

    docker exec ulaknode ulaknode-mailbox show alice@yourdomain.com

    Remove a mailbox:

    docker exec ulaknode ulaknode-mailbox remove alice@yourdomain.com
  • 🔗 How do I manage email aliases?

    Use the ulaknode-alias tool to create and manage address aliases. An alias forwards mail from a source address to one or more destination addresses.

    # Add an alias
    docker exec ulaknode ulaknode-alias add info@yourdomain.com alice@yourdomain.com
    
    # List all aliases for a domain
    docker exec ulaknode ulaknode-alias list yourdomain.com
    
    # Show the destination(s) for a specific alias
    docker exec ulaknode ulaknode-alias show info@yourdomain.com
    
    # Remove an alias
    docker exec ulaknode ulaknode-alias remove info@yourdomain.com

    Both the source and destination must be full email addresses. You can point the same source alias at multiple destinations by running ulaknode-alias add multiple times with different destinations.

  • 🔗 Can a separate web UI let users change their own password or quota?

    ulaknode-agent is a small TCP listener, started automatically alongside the other services, that lets a separate sidecar web UI drive password and quota changes without needing a shared filesystem or docker exec access into the ulaknode container. It speaks a one-line request/response protocol and runs ulaknode-mailbox passwd/quota-set locally, where doveadm/postmap already live.

    You don't have to build that sidecar yourself — itefixnet/ulaknode-web is the maintained reference implementation: a self-service /password/reset page (single-use, 30-minute emailed link) and a password-protected /admin panel for looking up a mailbox's current maildir/quota, resetting its password, or changing its quota. It talks to ulaknode-agent exactly as described below and holds no local copy of mailbox state.

    docker run -d \
      --name ulaknode-web \
      --network host \
      -e SECRET_KEY=change-me \
      -e ADMIN_PASSWORD=change-me \
      -e MAIL_FROM=no-reply@yourdomain.example \
      itefixnet/ulaknode-web:latest

    --network host is the simplest way to reach ulaknode-agent and Postfix, and works as long as the main ulaknode container also runs with network_mode: host. On a bridge-network deployment, point ULAKNODE_AGENT_HOST/SMTP_HOST at an address that reaches the ulaknode container instead, and widen ULAKNODE_AGENT_ALLOWED_IPS/Postfix's mynetworks on that side to match.

    It's opt-in in effect: nothing calls it unless you point a sidecar at it. By default it binds to loopback only (127.0.0.1:8787) and accepts connections only from 127.0.0.1/32. To widen it for a sidecar on a Docker bridge network, set these environment variables on the container:

    • ULAKNODE_AGENT_BIND — interface to bind to (default 127.0.0.1)
    • ULAKNODE_AGENT_ALLOWED_IPS — CIDR allowed to connect (default 127.0.0.1/32)
    • ULAKNODE_AGENT_PORT — listening port (default 8787)

    Protocol: a client sends one line, gets one line back. Email/password/quota values are base64-encoded so they can't collide with the field separator:

    PASSWD    <b64 email> <b64 password>
    QUOTA-SET <b64 email> <b64 limit>
    SHOW      <b64 email>

    SHOW returns the mailbox's current settings (maildir, quota limit) so a sidecar UI can read them instead of guessing or caching them locally. The response is OK: <message> or ERROR: <message>.

    If you don't run a sidecar UI, there's nothing to configure here — the listener stays bound to loopback and unused.

  • 🔗 How do I install or renew a TLS certificate?

    Use the ulaknode-cert tool to install a certificate (e.g. from Let's Encrypt) and verify it is correctly configured.

    Install a certificate:

    docker exec ulaknode ulaknode-cert install /tmp/fullchain.pem /tmp/privkey.pem

    Show the currently installed certificate:

    docker exec ulaknode ulaknode-cert show

    Verify the certificate and key match and check expiry:

    docker exec ulaknode ulaknode-cert check

    This command warns if the certificate expires within 14 days.

    Restart services after installing a new certificate:

    docker exec ulaknode ulaknode-service restart postfix
    docker exec ulaknode ulaknode-service restart dovecot
  • 🔗 How does spam filtering work, and how can I tune it?

    ulaknode filters spam in layers rather than through one unified scoring engine, using a set of focused, well-known tools:

    • Postscreen checks the connecting IP against DNSBL blocklists (Spamhaus Zen, SpamCop) before a real SMTP session even starts, using a dedicated recursive resolver (Unbound) so queries aren't misclassified as coming from a shared/open resolver
    • policyd-spf checks the sender's SPF record
    • OpenDKIM and OpenDMARC verify DKIM signatures and enforce the sender domain's DMARC policy
    • Postgrey greylists unfamiliar senders, temporarily deferring first-contact mail — real mail servers retry automatically per the SMTP spec, but most spambots don't
    • SpamAssassin scores the remaining mail's content (headers, body text, Bayesian statistics); messages above the configured threshold are tagged or rejected

    Train the Bayes classifier with known spam and ham:

    docker exec ulaknode sa-learn --spam /path/to/spam.eml
    docker exec ulaknode sa-learn --ham /path/to/ham.eml

    Common tuning via SpamAssassin configuration (edit /etc/spamassassin/local.cf):

    • Whitelist a sender: add whitelist_from address@example.com
    • Blacklist a sender: add blacklist_from address@example.com
    • Adjust the reject/tag threshold: edit required_score (default: 5.0)

    After editing configuration, reload SpamAssassin:

    docker exec ulaknode ulaknode-service reload spamassassin

    To bypass greylisting for a known-good sender, add their address or network to /etc/postgrey/whitelist_clients.local (this .local file is read alongside the Debian package's own maintained whitelist, rather than overwriting it) and restart Postgrey:

    docker exec ulaknode ulaknode-service restart postgrey

    DNSBL checking is on by default and needs no setup. To tune it, edit postscreen_dnsbl_sites, postscreen_dnsbl_threshold, or postscreen_dnsbl_action in /etc/postfix/main.cf, then reload Postfix. Unbound's own settings (cache size, logging) live in /etc/unbound/unbound.conf.d/ulaknode.conf — restart Unbound after editing:

    docker exec ulaknode ulaknode-service reload postfix
    docker exec ulaknode ulaknode-service restart unbound

    If mail starts getting rejected with "Service unavailable" or DNSBL false positives, check docker exec ulaknode ulaknode-errlog unbound — this usually means outbound port 53 is firewalled, so Unbound can't resolve recursively.

  • 🔗 How does brute-force protection (Fail2ban) work?

    ulaknode runs Fail2ban inside the container as a supervised service — no separate install or host bind-mounts needed. Rather than writing iptables rules (which don't work from inside a container's network namespace and would require NET_ADMIN/--privileged), it bans at the application layer by editing the same access-control tables Postfix and Dovecot already read: a Postfix cidr access table for SMTP, and a filter-scoped Dovecot deny passdb for IMAP.

    Two jails run by default:

    • ulaknode-smtp — watches /var/log/mail.log for SASL auth failures and unauthorized RCPT attempts; a ban blocks SMTP (25/587) only
    • ulaknode-imap — watches /var/log/dovecot.log for IMAP login failures; a ban blocks IMAP (993) only

    Both default to maxretry = 5, findtime = 10m, bantime = 1h.

    Check ban status:

    docker exec ulaknode fail2ban-client status
    docker exec ulaknode fail2ban-client status ulaknode-smtp
    docker exec ulaknode fail2ban-client status ulaknode-imap

    Unban an IP:

    docker exec ulaknode fail2ban-client set ulaknode-smtp unbanip <ip>
    docker exec ulaknode fail2ban-client set ulaknode-imap unbanip <ip>

    Tune the jails by editing /etc/fail2ban/jail.d/ulaknode.conf (persisted in the ulak_fail2ban volume), then reloading:

    docker exec ulaknode vi /etc/fail2ban/jail.d/ulaknode.conf
    docker exec ulaknode ulaknode-service reload fail2ban

    For progressive escalation on repeat offenders, add to each jail:

    bantime.increment = true
    bantime.multiplier = 24
    bantime.maxtime = 30d

    Note this stops credential-stuffing and brute-force login attempts, not volumetric floods — a banned IP still completes the TCP handshake and gets an instant reject. Actual flood/DoS protection is a host- or network-edge concern (rate-limiting at your firewall or provider).

  • 🔗 How do I troubleshoot a message that was not delivered?

    Start with the error log tool, then inspect the mail queue.

    View recent Postfix errors:

    docker exec ulaknode ulaknode-errlog postfix
    docker exec ulaknode ulaknode-errlog postfix 100

    The optional second argument sets how many lines to show (default: 50). The same command works for dovecot, opendkim, opendmarc, spamassassin, spamass-milter, clamav, clamav-milter, postgrey, fail2ban, and unbound.

    View the mail queue:

    docker exec ulaknode mailq

    Inspect a specific queued message by its queue ID:

    docker exec ulaknode postcat -q QUEUEID

    Common causes of non-delivery:

    • Deferred (4xx): temporary issue on the remote side — Postfix will retry automatically
    • Bounced (5xx): permanent rejection — read the DSN for the reason (often a missing PTR record, SPF failure, or blocklist listing)
    • Spam-filtered locally: check ulaknode-errlog spamassassin or ulaknode-errlog postgrey to see if the message was tagged, greylisted, or rejected before reaching the queue

    If you need further assistance, open a ticket at the support portal with the relevant log excerpts.

  • 🔗 How do I back up and restore ulaknode data?

    ulaknode stores all persistent data in named Docker volumes: mailboxes, logs, TLS certificates, and configuration for Postfix, Dovecot, OpenDKIM, OpenDMARC, SpamAssassin, policyd-spf, Postgrey, ClamAV, Fail2ban, and Unbound. A reliable backup strategy must cover all of these volumes.

    We recommend docker-volume-backup-borg, a purpose-built tool that automates encrypted, compressed backups of Docker volumes using BorgBackup. It discovers volumes automatically from running containers and supports both local and remote (SSH) repositories.

    Basic backup run targeting the ulaknode container:

    docker run --rm \
      --env-file .env \
      -e BACKUP_CONTAINERS=ulaknode \
      -v /var/run/docker.sock:/var/run/docker.sock \
      -v /var/lib/docker/volumes:/var/lib/docker/volumes:ro \
      -v ~/.ssh:/root/.ssh:ro \
      itefixnet/docker-volume-backup-borg

    Configure at minimum these variables in your .env file:

    • BORG_REPO — destination repository (local path or user@host:path for remote)
    • BORG_PASSPHRASE — encryption passphrase; store this securely, without it restores are impossible
    • KEEP_DAILY / KEEP_WEEKLY / KEEP_MONTHLY — retention policy (defaults: 7 / 4 / 6)

    Stop the container during backup for a fully consistent snapshot:

    docker run --rm \
      --env-file .env \
      -e BACKUP_CONTAINERS=ulaknode \
      -e STOP_CONTAINERS=ulaknode \
      -v /var/run/docker.sock:/var/run/docker.sock \
      -v /var/lib/docker/volumes:/var/lib/docker/volumes:ro \
      itefixnet/docker-volume-backup-borg

    List available backups:

    docker run --rm --env-file .env \
      itefixnet/docker-volume-backup-borg list

    Restore a specific archive:

    docker run --rm --env-file .env \
      -v $(pwd):/restore \
      itefixnet/docker-volume-backup-borg extract ARCHIVE_NAME

    Keep the .env file and your passphrase in a safe, separate location from the backup repository itself.

  • 🔗 How do I monitor ulaknode's health and mail flow?

    itefixnet/ulaknode-exporter is a maintained Prometheus exporter that runs as a sidecar next to the main ulaknode container. It tails the same log files and reads the same data volumes (mounted read-only, never written to) and exposes counters/gauges for Postfix/Postscreen (DNSBL hits, SASL auth failures, delivery status), Dovecot (logins, failures, active sessions), ClamAV/freshclam, SpamAssassin, Postgrey, and Fail2ban, plus basic host resource usage (load, memory, disk, network). No third-party dependencies — pure Python standard library.

    docker run -d \
      --name ulaknode-exporter \
      -p 9101:9101 \
      -v ulak_logs:/var/log:ro \
      -v ulak_mail:/var/mail:ro \
      -v ulak_clamav:/var/lib/clamav:ro \
      itefixnet/ulaknode-exporter:latest

    Metrics are served on /metrics (default port 9101):

    curl http://localhost:9101/metrics

    This assumes the ulak_logs, ulak_mail, and ulak_clamav volumes already exist — created automatically by the main ulaknode container on first run. A ready-made Grafana dashboard covering every component (an always-visible overview row plus per-component rows collapsed by default) ships alongside the exporter's source.

    By default the exporter runs in its own network namespace, so its network byte-count metrics describe its own virtual interface, not the host's real NIC traffic. Run it with network_mode: host to report real host network throughput — the same tradeoff the standard Prometheus node_exporter makes — at the cost of network isolation.

  • 🔗 Is ulaknode really free? Are there hidden per-user fees?

    Yes, ulaknode is genuinely free. It is released under a permissive BSD-style license with no per-user, per-mailbox, or per-domain charges. You can run it for any number of users on your own hardware at no cost.

    Optional commercial support plans are available starting at $549/year for organisations that want professional assistance with deployment, DNS configuration, migration, maintenance, and spam-filtering optimisation. Multi-year and volume discounts are available.

    To learn more about support options or to request a quote, reach us via our contact form.