Skip to content

Stalwart Stalwart

Description / nameInput element
Container Registry
Container Configuration Root Path
Timezone
Stalwart Host Port
Stalwart /config Path

Build Status Last Commit

Stalwart Mail Server is an open-source mail server solution with JMAP, IMAP4, POP3, and SMTP support and a wide range of modern features. It is written in Rust and designed to be secure, fast, robust and scalable.

Port 25
Registry ghcr.io/daemonless/stalwart
Daemonless daemonless/stalwart
Source stalw.art/
Website stalw.art

Version Tags

Tag Description Best For
latest Upstream Binary. Built from official release. Most users — recommended.
pkg FreeBSD Quarterly. Uses stable, tested packages. Production stability.
pkg-latest FreeBSD Latest. Rolling package updates. Staying current.

Before deploying, ensure your host environment is ready. See the Quick Start Guide for host setup instructions, including the security model for host vs. container privileges.

Deployment

services:
  stalwart:
    image: "ghcr.io/daemonless/stalwart:latest"
    container_name: stalwart
    environment:
      - TZ=UTC  # Timezone for the container
      - ADMIN_SECRET=changeme  # Password for the fallback 'admin' web-admin account
      - ENABLE_V4PROXY=true  # Enable internal socat IPv4->IPv6 proxy on port 8080 for initial setup UI and healthchecks on FreeBSD (default true). Set false to disable.
      - SKIP_CHOWN=true  # Skip the startup recursive chown of /config once ownership is recorded in /config/.chown_done (default true). Set false to force a chown on every start. The marker lives in /config, so /config must be a persistent volume for the skip to take effect across restarts.
    volumes:
      - "/path/to/containers/stalwart:/config"
    ports:
      - "25:25"
      - "465:465"
      - "587:587"
      - "143:143"
      - "993:993"
      - "110:110"
      - "995:995"
      - "4190:4190"
      - "443:443"
      - "8080:8080"
    # always (not unless-stopped) so FreeBSD's podman rc.d auto-starts it at boot
    restart: always
podman run -d --name stalwart \
  -p 25:25 \
  -p 465:465 \
  -p 587:587 \
  -p 143:143 \
  -p 993:993 \
  -p 110:110 \
  -p 995:995 \
  -p 4190:4190 \
  -p 443:443 \
  -p 8080:8080 \
  -e TZ=UTC \
  -e ADMIN_SECRET=changeme \
  -e ENABLE_V4PROXY=true \
  -e SKIP_CHOWN=true \
  -v /path/to/containers/stalwart:/config \
  ghcr.io/daemonless/stalwart:latest
- name: Deploy stalwart
  containers.podman.podman_container:
    name: stalwart
    image: "ghcr.io/daemonless/stalwart:latest"
    state: started
    restart_policy: always
    env:
      TZ: "UTC"
      ADMIN_SECRET: "changeme"
      ENABLE_V4PROXY: "true"
      SKIP_CHOWN: "true"
    ports:
      - "25:25"
      - "465:465"
      - "587:587"
      - "143:143"
      - "993:993"
      - "110:110"
      - "995:995"
      - "4190:4190"
      - "443:443"
      - "8080:8080"
    volumes:
      - "/path/to/containers/stalwart:/config"
1
2
3
4
5
6
7
# .env

DIRECTOR_PROJECT=stalwart
TZ=UTC
ADMIN_SECRET=changeme
ENABLE_V4PROXY=true
SKIP_CHOWN=true
# appjail-director.yml

options:
  - virtualnet: ':<random> default'
  - nat:
services:
  stalwart:
    name: stalwart
    options:
      - container: 'boot args:--pull'
      - expose: '25:25 proto:tcp'
      - expose: '465:465 proto:tcp'
      - expose: '587:587 proto:tcp'
      - expose: '143:143 proto:tcp'
      - expose: '993:993 proto:tcp'
      - expose: '110:110 proto:tcp'
      - expose: '995:995 proto:tcp'
      - expose: '4190:4190 proto:tcp'
      - expose: '443:443 proto:tcp'
      - expose: '8080:8080 proto:tcp'
    oci:
      user: root
      environment:
        - TZ: !ENV '${TZ}'
        - ADMIN_SECRET: !ENV '${ADMIN_SECRET}'
        - ENABLE_V4PROXY: !ENV '${ENABLE_V4PROXY}'
        - SKIP_CHOWN: !ENV '${SKIP_CHOWN}'
    volumes:
      - STALWART_CONFIG_PATH: /config
volumes:
  STALWART_CONFIG_PATH:
    device: '/path/to/containers/stalwart'
1
2
3
4
5
6
# Makejail

ARG tag=latest

OPTION overwrite=force
OPTION from=ghcr.io/daemonless/stalwart:${tag}
appjail oci run -Pd \
  -o overwrite=force \
  -o container="args:--pull" \
  -o virtualnet=":<random> default" \
  -o nat \
  -o expose="25:25 proto:tcp" \
  -o expose="465:465 proto:tcp" \
  -o expose="587:587 proto:tcp" \
  -o expose="143:143 proto:tcp" \
  -o expose="993:993 proto:tcp" \
  -o expose="110:110 proto:tcp" \
  -o expose="995:995 proto:tcp" \
  -o expose="4190:4190 proto:tcp" \
  -o expose="443:443 proto:tcp" \
  -o expose="8080:8080 proto:tcp" \
  -e TZ=UTC \
  -e ADMIN_SECRET=changeme \
  -e ENABLE_V4PROXY=true \
  -e SKIP_CHOWN=true \
  -o fstab="/path/to/containers/stalwart /config <pseudofs>" \
  ghcr.io/daemonless/stalwart:latest stalwart

Access at: http://localhost:25

Interactive Configuration

Parameters

Environment Variables

Variable Default Description
TZ UTC Timezone for the container
ADMIN_SECRET changeme Password for the fallback 'admin' web-admin account
ENABLE_V4PROXY true Enable internal socat IPv4->IPv6 proxy on port 8080 for initial setup UI and healthchecks on FreeBSD (default true). Set false to disable.
SKIP_CHOWN true Skip the startup recursive chown of /config once ownership is recorded in /config/.chown_done (default true). Set false to force a chown on every start. The marker lives in /config, so /config must be a persistent volume for the skip to take effect across restarts.

Volumes

Path Description
/config Config (config.toml) + rocksdb data store

Ports

Port Protocol Description
25 TCP SMTP
465 TCP SMTP submission (implicit TLS)
587 TCP SMTP submission (STARTTLS)
143 TCP IMAP4
993 TCP IMAP4 (implicit TLS)
110 TCP POP3
995 TCP POP3 (implicit TLS)
4190 TCP ManageSieve
443 TCP HTTPS / JMAP / web admin
8080 TCP HTTP / JMAP / web admin

IPv4 / Dual-Stack & Internal Proxy Notes

  • Internal IPv4 Proxy (ENABLE_V4PROXY): Stalwart v0.16.x bootstrap mode binds to [::]:8080 (IPv6). On FreeBSD (where net.inet6.ip6.v6only=1 by default), accessing http://127.0.0.1:8080 via IPv4 is refused. The container includes an internal socat helper (stalwart-v4proxy) enabled by default (ENABLE_V4PROXY=true) that bridges IPv4 port 8080 to IPv6 [::1]:8080. You can disable the internal proxy by setting ENABLE_V4PROXY=false, or enable IPv4-mapping host-wide on FreeBSD by setting sysctl net.inet6.ip6.v6only=0.
  • External IPv4 Reverse Proxy / PROXY Protocol: If placing Stalwart behind an external IPv4 reverse proxy (Nginx, Traefik, HAProxy) or using PROXY protocol headers, add your proxy's IPv4 address/subnet (e.g. 127.0.0.1/32) to proxyTrustedNetworks under Settings → Network → Services in Stalwart to prevent header parsing failures and proxy auto-bans.

Using PostgreSQL or SQLite instead of RocksDB

By default config.toml stores everything (data, full-text search, blobs, and lookups) in RocksDB under /config/data. To swap in an external PostgreSQL database instead, replace the [store."rocksdb"] section and point [storage] at the new store:

[storage]
data = "postgresql"
fts = "postgresql"
blob = "postgresql"
lookup = "postgresql"
directory = "internal"

[store."postgresql"]
type = "postgresql"
host = "postgres-host"
port = 5432
database = "stalwart"
user = "stalwart"
password = "changeme"
pool.max-connections = 10

[directory."internal"]
type = "internal"
store = "postgresql"

For SQLite (a single-file database, good for small single-node setups):

[storage]
data = "sqlite"
fts = "sqlite"
blob = "sqlite"
lookup = "sqlite"
directory = "internal"

[store."sqlite"]
type = "sqlite"
path = "/config/stalwart.db"
pool.max-connections = 10

[directory."internal"]
type = "internal"
store = "sqlite"

Edit /config/config.toml after the first run seeds it, then restart the container. Blob storage can also be split onto a separate store (e.g. keep blob = "rocksdb" while data/fts/lookup move to PostgreSQL) — see stalw.art/docs/storage for the full per-store breakdown.

Implementation Details

  • Architectures: amd64
  • User: stalwart (UID/GID set via PUID/PGID). Defaults to 1000:1000.
  • Base: Built on ghcr.io/daemonless/base (FreeBSD 15.1).

Need help? Join our Discord community.