mix d6e67ceb61 panel: set Sign out like the entries it stands among
The compact button rule is written for controls that cluster — several beside a
value or inside a table cell — where 0.8rem/600 is what stops four of them
shouting at once. Sign out borrows that rule for its colours, and was taking
the type with it: in the navigation column it has no cluster to belong to, so
it stood under Settings among entries set 0.95rem/400 as the one smaller,
heavier word on the column, reading as a different kind of object rather than
as the last item of the list.

Type and padding go back to the entries'. The red, the border and the tinted
background stay — those are what say it acts, and the restyle only made the
mismatch easier to see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 00:59:08 +03:00
2026-08-10 21:56:51 +03:00
2026-08-10 21:56:51 +03:00
2026-08-10 23:30:46 +03:00
2026-08-11 00:56:49 +03:00
2026-08-09 12:33:29 +03:00
2026-08-09 12:33:29 +03:00
2026-08-10 23:30:46 +03:00
2026-08-09 22:32:09 +03:00

SelfPost

SelfPost

Self-hosted outbound SMTP relay with a web control panel, shipped as a single Docker image. Postfix, OpenDKIM, and a small Go panel run together under supervisord; you configure domains, DKIM keys, and SASL applications once, then point your apps at the SMTP endpoint.

SelfPost sends mail straight to the internet from your own IP, with per-domain DKIM signing. It is outbound only — no inbound mail, mailboxes, or webmail.

For: operators who run their own VPS or home server and want a simple relay they control, without a third-party SMTP provider.

Key properties: one container, multi-domain DKIM, SASL per application, send log and DNS checks in the panel, encrypted backups.

Features

  • Outbound SMTP (465/smtps; optional 587 submission) with per-domain DKIM signing
  • Web panel — domains, applications, deliveries, mail queue, system log, backup
  • Multi-domain relay — each SASL application is bound to one sending domain
  • DNS status checks (PTR, SPF, DKIM, DMARC) with in-panel re-check
  • Two-level rate limiting — IP backstop (Postfix) and per-domain/per-app limits
  • Full-server backup and single-domain export/import (optional password encryption)
  • Single Docker image; data in a ./data bind mount

Documentation

Document Contents
Operator guide Reverse proxy, environment variables, DNS, IP warmup, panel operations, rate limiting, backup/restore, ports, image tag
Product boundaries Purpose, deployment assumptions, out-of-scope items, multi-domain model
Architecture As-built technical design
Security design Mandatory requirements, accepted risks, the CSRF ADR
Development Building, testing, docs rules, model routing, commits
Roadmap Open work (1.x+) — direction, not commitments
CHANGELOG Release history

Found a vulnerability? Do not open an issue — SECURITY.md has the private reporting channel and the scope.

Repository: https://github.com/mixeme/selfpost — source, issues, releases, and the ghcr.io/mixeme/selfpost image.

Requirements

Providing these is the operator's job — SelfPost cannot fix a blocked port or a missing PTR record for you. Details: Operator guide.

Platform

  • Docker + Compose v2 on the host
  • A reverse proxy in front of the panel (SelfPost never terminates HTTPS itself)
  • Rough sizing: 1 vCPU, 512 MB1 GB RAM, 810 GB disk (send log and rotated mail.log are the main growth drivers; both sit in the ./data volume, and both are capped — 90 days and 14 files by default)

Network and IP

  • Static IP address
  • Outbound TCP port 25 unblocked (many consumer/cloud hosts block it by default)
  • PTR/rDNS for that IP pointing at your mail hostname (SELFPOST_HOSTNAME)
  • Reasonable starting IP reputation — a fresh IP still needs warmup

Per sending domain

For every domain you add in the panel:

  • SPF TXT record authorizing this server
  • DKIM TXT record (value shown on the domain page)
  • DMARC _dmarc TXT record

See DNS setup in the operator guide.

Quick start

First boot — create the admin account. On a fresh container SelfPost prints a one-time setup URL (valid ten minutes). Open it in a browser to choose the administrator username and password. Until you do, the panel has no login. Production deploy: step 3.

One container, panel at http://127.0.0.1:8080 — no reverse proxy, no TLS files, no compose files. Good for clicking through the UI on your machine; outbound mail will not reach the real internet without DNS, PTR, and port 25.

docker run --rm -d --name selfpost-try \
  -p 127.0.0.1:8080:8080 \
  -e SELFPOST_HOSTNAME=mail.local.test \
  -e PANEL_COOKIE_SECURE=false \
  -v selfpost-try-data:/data \
  ghcr.io/mixeme/selfpost:1.1.0

Get the setup URL (pick one):

docker logs selfpost-try 2>&1 | grep -m1 'http'
docker exec selfpost-try cat /data/setup-token

The printed URL is https://mail.local.test/setup/<token>. For this local trial rewrite it to http://127.0.0.1:8080/setup/<token> (same path token; PANEL_COOKIE_SECURE=false so the cookie works over plain HTTP). Open it before it expires.

When finished:

docker rm -f selfpost-try && docker volume rm selfpost-try-data

More detail (limitations, optional throwaway TLS for local SMTP): Local trial in the operator guide.

Reference deploy

Production layout: one docker-compose.yml, a .env, persistent ./data, and TLS PEM files at ./certs (read by Postfix on 465/587). The panel is reached only through a reverse proxy on 443 — port 8080 is bound to localhost in the default compose file.

Artefact Path
Compose file (fixed image tag) deploy/docker-compose.yml
Environment template deploy/.env.example
Apache vhost (recommended) deploy/apache/selfpost-vhost.conf
nginx deploy/nginx/
Caddy deploy/caddy/
Traefik deploy/traefik/

1. Fetch the base files

mkdir -p selfpost/data selfpost/certs && cd selfpost
curl -O https://raw.githubusercontent.com/mixeme/selfpost/main/deploy/docker-compose.yml
curl -O https://raw.githubusercontent.com/mixeme/selfpost/main/deploy/.env.example
cp .env.example .env

Edit .env — at minimum set SELFPOST_HOSTNAME to your mail hostname (bare FQDN, e.g. mail.example.com). It must match the PTR record you request from your provider and the certificate your proxy will obtain.

2. Reverse proxy and TLS

Pick one proxy. In every case the proxy terminates HTTPS for the panel; the same certificate must end up under ./certs as fullchain.pem and privkey.pem so Postfix can serve it on 465 (and 587 if enabled). The proxy must pass the original Host header — details and rationale: Reverse proxy.

Apache (recommended, on the host). Install Apache with ssl, proxy, and proxy_http enabled. Copy deploy/apache/selfpost-vhost.conf into your vhost directory, replace mail.example.com with your hostname, enable the site, then issue a certificate:

sudo certbot --apache -d mail.example.com

Point ./certs at the PEM files certbot wrote (symlink is fine):

ln -s /etc/letsencrypt/live/mail.example.com certs

nginx (containerised). From the deploy/ directory, merge the nginx fragment and issue the first certificate before nginx can serve HTTPS:

docker compose -f docker-compose.yml -f nginx/docker-compose.nginx.yml \
  run --rm certbot certonly --webroot -w /var/www/certbot \
  -d mail.example.com --email you@example.com --agree-tos --no-eff-email

docker compose -f docker-compose.yml -f nginx/docker-compose.nginx.yml up -d

Edit deploy/nginx/nginx.conf.example and replace mail.example.com first. The fragment bind-mounts certbot's output into both nginx and SelfPost.

Caddy (containerised, automatic ACME). Edit deploy/caddy/Caddyfile and the <hostname> placeholders in deploy/caddy/docker-compose.caddy.yml, then:

docker compose -f docker-compose.yml -f caddy/docker-compose.caddy.yml up -d

Verify Caddy's on-disk cert path for your version before relying on the default mount — see the comment at the top of the Caddy compose fragment.

Traefik (containerised). Edit the Host(...) label and ACME email in deploy/traefik/docker-compose.traefik.yml, start the stack, then extract PEM files for Postfix whenever Traefik issues or renews a certificate:

docker compose -f docker-compose.yml -f traefik/docker-compose.traefik.yml up -d
./traefik/extract-cert.sh ./traefik/letsencrypt/acme.json mail.example.com ./traefik/extracted-certs

Schedule extract-cert.sh (cron or a timer) alongside Traefik's renewals.

3. Start SelfPost

If you used Apache on the host (step 2, first option), start only the base compose file from your selfpost/ directory:

docker compose up -d

The nginx/Caddy/Traefik fragments from step 2 already include docker compose up -d — skip this if you ran one of those.

Get the setup URL — open it in a browser to create the admin account (first boot):

docker compose logs selfpost 2>&1 | grep -m1 'http'
cat ./data/setup-token

The file is deleted as soon as setup completes. If logs are shipped to a central aggregator, prefer cat ./data/setup-token so the bearer token does not enter the log pipeline.

4. DNS and sending

Before sending real mail:

  1. Confirm PTR/rDNS for the server IP points at SELFPOST_HOSTNAME (Status page → Re-check).
  2. For each domain you add in the panel, publish SPF, DKIM, and DMARC at the same time (DNS setup).
  3. Warm up a new IP gradually (IP warmup).

Ports and upgrades

The compose file maps 465 (always) and 587 (when SUBMISSION_ENABLE=true). Bump the pinned image tag deliberately when upgrading — never use :latest (why).

Optional variables (TRUSTED_PROXY_CIDR, rate limits, retention): see Environment variables.

License

Copyright © 2026 Mikhail Yenuchenko.

AGPL-3.0. The AGPL closes the "SaaS loophole": if you run a modified version as a network-accessible service, you must make the modified source available to its users — not only when you distribute copies of the code. Third-party notices: NOTICE.

S
Description
No description provided
Readme AGPL-3.0 10 MiB
Languages
Go 83%
HTML 7%
CSS 5.3%
Shell 2.7%
JavaScript 0.8%
Other 1.2%