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 / pkg FreeBSD Quarterly. Uses stable, tested packages. Most users. Matches Linux Docker behavior.
pkg-latest FreeBSD Latest. Rolling package updates. Newest FreeBSD packages.

Root Privileges Required

Podman on FreeBSD currently requires root. All commands must be run as root (or via doas/sudo).

Before deploying, ensure your host environment is ready. See the Quick Start Guide for host setup instructions.

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
    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"
    restart: unless-stopped
1
2
3
4
5
# .env

DIRECTOR_PROJECT=stalwart
TZ=UTC
ADMIN_SECRET=changeme
# 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}'
    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}
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 \
  -v /path/to/containers/stalwart:/config \
  ghcr.io/daemonless/stalwart:latest
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 \
  -o fstab="/path/to/containers/stalwart /config <pseudofs>" \
  ghcr.io/daemonless/stalwart:latest stalwart
- 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"
    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"

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

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

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.