- Go 98.9%
- Dockerfile 1.1%
| .forgejo/workflows | ||
| cmd/dashboard | ||
| internal | ||
| vendor | ||
| .env.example | ||
| .gitignore | ||
| CHANGELOG.md | ||
| docker-compose.prod.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
| VERSION | ||
Postfix Dashboard
A self-contained web dashboard to supervise a Postfix server running in Docker. Single Go binary (Chart.js embedded, no CDN for scripts), SQLite storage, no external runtime.
Features
| Area | Details |
|---|---|
| Authentication | Admin login, bcrypt, hashed server-side sessions (12 h), login rate limiting |
| Overview | KPI cards with trend arrows (vs yesterday / previous week), hourly and weekly charts |
| Analytics | 7/30/45/90 day trends, volume, delivery rate, hour x weekday heatmap, bounce rate by domain, top senders / recipients / relays / domains |
| Log History | 30/45+ days of deliveries: date, sender, recipient, subject, recipients count, relay, size, status, SMTP code. Search (sender, recipient, queue ID, subject) over the whole database, status filter, pagination, refresh |
| Live Logs | Real-time WebSocket stream of the container output, colouring, filter, auto-scroll |
| Queue | List, hold, release, delete, flush, delete all (executed in the Postfix container through the Docker API) |
| Settings | Database statistics (records, size, oldest/newest), retention, manual purge, automatic daily purge at 02:00 |
Logs are read from the container output (Docker API over the socket) - no log file is needed.
Quick start
cp .env.example .env # edit it (password, DOCKER_GID, TZ, ...)
docker compose -f docker-compose.prod.yml up -d --build
# open http://localhost:8080
DOCKER_GIDmust match the host docker group:getent group docker | cut -d: -f3.TZmust be the timezone of your Postfix container (Postfix log lines carry no year or timezone).- If
DASHBOARD_PASSis empty on the first start, a random password is generated and printed once in the container logs.
Pipeline build
A Forgejo workflow (.forgejo/workflows/docker-build.yml) builds and pushes the image on v* tags (secret REPO_TOKEN).
Other modes
# Pipe logs from stdin
docker logs -f postfix-1 2>&1 | ./postfix-dashboard -log-source stdin -db ./dash.db
# Seed from a historical log file
./postfix-dashboard -db ./dash.db -seed /var/log/mail.log
Configuration
| Flag | Env | Default | Description |
|---|---|---|---|
-addr |
- | :8080 |
Listen address |
-db |
- | /data/postfix-dashboard.db |
SQLite path |
-log-source |
LOG_SOURCE |
stdin |
stdin or docker://<container> |
-docker-socket |
- | /var/run/docker.sock |
Docker socket path |
-admin-user |
ADMIN_USER |
admin |
Admin username |
-admin-pass |
ADMIN_PASS |
random on first run | Admin password. When provided it is applied at every start and revokes existing sessions |
-retention |
- | 45 |
Retention in days. Only applied when passed explicitly or when nothing is stored yet; otherwise the value chosen in Settings is kept |
-seed |
- | - | Log file to import at startup |
-healthcheck |
- | - | Probe the local server and exit (used by the Docker HEALTHCHECK) |
| - | TZ |
UTC |
Timezone used to read Postfix timestamps and group statistics |
| - | TRUST_PROXY |
false |
true behind a reverse proxy you control: use X-Forwarded-For / X-Forwarded-Proto (client IP, Secure cookie) |
Subject column
Postfix does not log message subjects by default. To fill the Subject column, enable header logging in Postfix:
# main.cf
header_checks = regexp:/etc/postfix/header_checks
# header_checks
/^subject:/ INFO
Without it the column shows a dash.
Security
- Passwords: bcrypt. Constant-time username check and dummy hash to avoid user enumeration.
- Sessions: 32 random bytes, only the SHA-256 is stored in SQLite. Cookie
HttpOnly,SameSite=Strict,Securewhen served over HTTPS. The token is never exposed to JavaScript. - Login: 5 failures per IP / 15 minutes, then HTTP 429. Failures are logged as
[auth] login failed ip=...(usable by fail2ban). - WebSocket: single-use ticket valid 30 s (no token in URLs), same-origin check, connection cap, ping/pong timeouts.
- CSRF: state-changing endpoints reject cross-origin
Origin, JSON bodies capped at 16 KiB. - Headers: Content-Security-Policy,
X-Frame-Options: DENY,nosniff,Referrer-Policy: no-referrer,Cache-Control: no-storeon the API. - Queue actions: IDs validated (hex only), commands run without a shell, 30 s timeout, every action audit-logged with user and IP.
- SQL: parameterised queries,
LIKEwildcards in search are escaped. - Container: non-root user,
cap_drop: ALL,no-new-privileges, read-only root filesystem, database file mode0600.
Docker socket warning. Access to docker.sock is equivalent to root on the host, even when mounted :ro
(the flag does not restrict the API). The dashboard only uses log streaming and exec in the Postfix container.
For stricter isolation put a filtering proxy such as tecnativa/docker-socket-proxy in front (allow CONTAINERS and EXEC, POST=1) and point -docker-socket at it.
Always serve the dashboard behind HTTPS (Traefik, nginx...) and set TRUST_PROXY=true.
Layout
cmd/dashboard/ main.go (bootstrap, middleware), html.go / css.go / js.go (embedded UI), assets/chart.umd.js
internal/auth/ bcrypt, sessions, login limiter, WS tickets
internal/api/ REST + WebSocket routes
internal/parser/ Postfix log parser
internal/store/ SQLite data layer
internal/ws/ WebSocket hub
internal/queue/ postqueue / postsuper (local or Docker exec)
internal/dockerlogs Docker socket log streaming
Build and test
Requirements: Go 1.22+, gcc, libsqlite3-dev. Dependencies are vendored.
go test -mod=vendor ./...
go build -mod=vendor -o postfix-dashboard ./cmd/dashboard
Known limitations
- Containers started with a TTY do not use Docker's multiplexed log framing and are not supported as log source.
- The brute-force limiter and WebSocket tickets are in memory (reset on restart); run a single replica.
- Statistics are based on delivery attempts (one row per recipient and outcome), so a message deferred then sent produces two rows.
License
MIT