From 977d16494b581a082d58fbcf2204a810ebe61ea7 Mon Sep 17 00:00:00 2001 From: mixeme Date: Sat, 8 Aug 2026 10:44:37 +0300 Subject: [PATCH] docs: improve quick start and reference deploy instructions Co-authored-by: Cursor --- README.md | 184 +++++++++++++++++++++++++++++++++++++++++++++----- docs/guide.md | 29 ++++++++ 2 files changed, 195 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 3afcce6..060e7ef 100644 --- a/README.md +++ b/README.md @@ -75,29 +75,60 @@ See [DNS setup](docs/guide.md#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. +> How to read the link depends on the path below; production deploy: +> [step 3](#3-start-selfpost). + +### Try it locally + +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. + ```sh -mkdir -p selfpost && 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 -mv .env.example .env # then edit SELFPOST_HOSTNAME etc. -docker compose up -d +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:0.1.0 ``` -`SELFPOST_HOSTNAME` is required — bare FQDN only (e.g. `mail.example.com`). It -is the Postfix HELO name, the SASL realm, and must match the PTR record and -certificate CN/SAN. +**Get the setup URL** (pick one): -This starts SelfPost only. You still need a reverse proxy with TLS certificates -bind-mounted at `./certs` — see [Reference deploy](#reference-deploy) and the -[reverse-proxy section](docs/guide.md#reverse-proxy-mandatory) in the operator -guide. +```sh +docker logs selfpost-try 2>&1 | grep -m1 'http' +``` -On first start, `docker compose logs -f` prints a one-time setup link to create -the admin account. The same link is also in `./data/setup-token` on the host — -see [Operations → First-time setup link](docs/guide.md#operations). +```sh +docker exec selfpost-try cat /data/setup-token +``` + +Open the printed `http://…/setup?token=…` link before it expires. + +When finished: + +```sh +docker rm -f selfpost-try && docker volume rm selfpost-try-data +``` + +More detail (limitations, optional throwaway TLS for local SMTP): [Local +trial](docs/guide.md#local-trial) in the operator guide. + +### Deploy for real use + +Step-by-step production deploy (compose, reverse proxy, TLS, DNS): +[Reference deploy](#reference-deploy). ## 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](deploy/docker-compose.yml) | @@ -107,11 +138,128 @@ see [Operations → First-time setup link](docs/guide.md#operations). | Caddy | [deploy/caddy/](deploy/caddy/) | | Traefik | [deploy/traefik/](deploy/traefik/) | -The compose file maps ports **465** (always) and **587** (when -`SUBMISSION_ENABLE=true`). TLS certificate paths inside the container are fixed -to match the `./certs` bind mount. Bump the pinned image tag deliberately when +### 1. Fetch the base files + +```sh +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](docs/guide.md#reverse-proxy-mandatory). + +**Apache (recommended, on the host).** Install Apache with `ssl`, `proxy`, and +`proxy_http` enabled. Copy +[deploy/apache/selfpost-vhost.conf](deploy/apache/selfpost-vhost.conf) into your +vhost directory, replace `mail.example.com` with your hostname, enable the site, +then issue a certificate: + +```sh +sudo certbot --apache -d mail.example.com +``` + +Point `./certs` at the PEM files certbot wrote (symlink is fine): + +```sh +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: + +```sh +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](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](deploy/caddy/Caddyfile) and the `` placeholders +in [deploy/caddy/docker-compose.caddy.yml](deploy/caddy/docker-compose.caddy.yml), +then: + +```sh +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](deploy/traefik/docker-compose.traefik.yml), +start the stack, then extract PEM files for Postfix whenever Traefik issues or +renews a certificate: + +```sh +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: + +```sh +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](#quick-start)): + +```sh +docker compose logs selfpost 2>&1 | grep -m1 'http' +``` + +```sh +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](docs/guide.md#dns-setup)). +3. Warm up a new IP gradually ([IP warmup](docs/guide.md#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](docs/guide.md#fixed-image-tag)). +Optional variables (`TRUSTED_PROXY_CIDR`, rate limits, retention): see +[Environment variables](docs/guide.md#environment-variables). + ## License [AGPL-3.0](LICENSE). The AGPL closes the "SaaS loophole": if you run a modified diff --git a/docs/guide.md b/docs/guide.md index 55521f6..b84da9e 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -6,6 +6,7 @@ overview and quick start, see [README.md](../README.md). ## Table of contents - [Reverse proxy (mandatory)](#reverse-proxy-mandatory) +- [Local trial](#local-trial) - [Environment variables](#environment-variables) - [DNS setup](#dns-setup) - [IP warmup](#ip-warmup) @@ -50,6 +51,34 @@ and Traefik by default). A proxy that rewrites `Host` instead makes the panel reject every form submission as cross-origin — the log says so explicitly, printing the `Origin` and `Host` it compared. +## Local trial + +The [README quick start](../README.md#try-it-locally) runs a single container +with `PANEL_COOKIE_SECURE=false` and port 8080 published on localhost. No +reverse proxy, no `./certs` bind mount — Postfix still starts, but the Status +page will report missing TLS material until you mount PEM files at +`/etc/postfix/tls/fullchain.pem` and `privkey.pem`. + +**What works:** the full panel — setup, domains, applications, deliveries view, +mail queue, system log. **What does not:** reliable outbound delivery to the +public internet (no PTR, no real DNS for your domains, port 25 may be blocked +on your network, HELO does not match anything receivers trust). + +To exercise SMTP locally as well, generate a throwaway self-signed certificate +and mount it before `docker run`: + +```sh +mkdir -p /tmp/selfpost-certs +openssl req -x509 -newkey rsa:2048 \ + -keyout /tmp/selfpost-certs/privkey.pem \ + -out /tmp/selfpost-certs/fullchain.pem \ + -days 1 -nodes -subj '/CN=mail.local.test' +``` + +Add `-v /tmp/selfpost-certs:/etc/postfix/tls:ro` to the `docker run` command +(and keep `SELFPOST_HOSTNAME=mail.local.test` so it matches the certificate CN). +Clients must skip TLS verification — the cert is not from a public CA. + ## Environment variables Copy [deploy/.env.example](../deploy/.env.example) to `.env` next to your