Deployment and operations
PhoneMail runs on shankars, a Raspberry Pi 5 (8 GB, Debian 12) at home, which also runs the host's mail server (Postfix, Dovecot, OpenDKIM), Roundcube and a few other websites. A second Pi, jaihat26tops (16 GB), is the staging machine.
Where things live on shankars
| Item | Location |
|---|---|
| Source | /srv/data/phonemail/src |
| Settings and secrets | /srv/data/phonemail/src/.env (mode 600): database password, internal token, JWT secret |
| Twilio credentials | /etc/phonemail/twilio.env (root, group of the deploy user, 640) |
| Database files | /srv/data/phonemail/postgres (NVMe, not the SD card) |
| Database backups | /srv/data/phonemail/backups (taken before every migration) |
| Web client | /srv/data/phonemail/web |
| Documentation and welcome page | /srv/data/phonemail/site/docs, /srv/data/phonemail/site/phonemail.net |
| nginx sites | /etc/nginx/sites-available/mail.phonemail.net (+ snippets/phonemail.locations.conf), sites-available/phonemail.net |
| Certificates | Let's Encrypt mail.phonemail.net and phonemail.net, renewed automatically (webroot) |
| Postfix maps | /etc/postfix/phonemail_transport, phonemail_recipients, phonemail_access (written by deploy/postfix-phonemail.sh) |
| DKIM key | /etc/opendkim/keys/phonemail.net/mail.private |
DNS for phonemail.net
| Record | Value |
|---|---|
A @, CNAME www | the home IP; www redirects to phonemail.net |
A mail | the home IP |
MX @ | mail.phonemail.net, priority 10 |
TXT @ | v=spf1 mx ~all |
TXT _dmarc | v=DMARC1; p=quarantine; adkim=r; aspf=r |
TXT mail._domainkey | the OpenDKIM public key |
Records are managed through the registrar's API with deploy/godaddy-dns.sh (get, site, set), which backs up the zone before each change.
nginx routes (mail.phonemail.net)
| Path | Goes to |
|---|---|
/api/internal/ | 404: the mail service's event hook is never public |
/api/socket.io/ | api Socket.IO (WebSocket upgrade) |
/api/ | api, prefix removed |
/twilio/ | api, path kept so Twilio's signature matches |
/webmail/ | Roundcube |
/docs/ | this site |
/assets/ | hashed web assets, cached for a year |
| everything else | the web client (index.html) |
phonemail.net itself serves only the welcome page and /docs/, with its own request limit, so casual visitors never compete with the app.
Mail routing
- Incoming: Postfix accepts
*@phonemail.net. Before saying yes it asks PhoneMail (reject_unverified_recipient), so unknown numbers and aliases get550during the SMTP conversation and no bounces are sent to forged senders. Accepted mail goes to the mail service on127.0.0.1:2525, which parses MIME (multipart, quoted-printable, base64, charsets, HTML to text), threads byIn-Reply-To, and stores a message once perMessage-ID. - Outgoing: messages with outside recipients get an
outboxrow. The worker renders the email (plain text, plus HTML when present, Bcc never in headers) and hands it to Postfix on the Docker gateway; OpenDKIM signs it for phonemail.net. Retries back off from one minute, doubling, up to 8 attempts; a 5xx answer is final. - Webmail: Roundcube's
phonemailplugin sends logins of the form<10 digits>@phonemail.netto PhoneMail's IMAP, submission and password ports, which listen only on the Docker bridge address.
Settings (.env)
| Variable | Meaning |
|---|---|
POSTGRES_PASSWORD, INTERNAL_TOKEN, JWT_SECRET | Generated once per machine; never committed |
MAIL_DOMAIN | phonemail.net |
PUBLIC_BASE_URL | https://mail.phonemail.net (used to check Twilio signatures) |
API_BIND | 127.0.0.1 in production; 0.0.0.0 on staging so phones on the Wi-Fi can reach it |
PG_DATA_DIR | /srv/data/phonemail/postgres |
RESERVED_ALIASES | Names of ordinary mailboxes on the same server, which PhoneMail must not take |
SMTP_RELAY | 172.28.0.1:25, the host's Postfix |
WEBMAIL_BIND | 172.17.0.1, where IMAP, submission and the password endpoint listen for Roundcube |
TWILIO_FROM_NUMBER | +15078000027 |
TWILIO_VERIFY_SID | Twilio Verify service; empty = OTP off, password login shown |
SMS_ALERTS, SMS_ALERT_BODY | New-mail texts; off by default |
Deploying
# 1. copy the source to /srv/data/phonemail/src (scp), then on shankars:
cd /srv/data/phonemail/src
bash deploy/deploy-prod.sh # compose up --build, web build, nginx, checks
bash deploy/prod-check.sh # non-destructive: api, password change, port-25 recipient checks
sudo bash deploy/dkim-probe-all.sh # confirms every sender's mail is DKIM-signed
deploy-prod.sh is safe to re-run: it creates .env only once, swaps the web build atomically (web.old kept), tests nginx before reloading and disables the site if the test fails. prod-check.sh uses one throwaway account and deletes exactly that account afterwards.
Day-to-day
docker compose ps # health of postgres, mail, api
docker compose logs -f api mail # logs
docker compose restart api # restart one service
sudo mailuser list # PhoneMail accounts and ordinary mailboxes
sudo mailuser passwd 9884690127@phonemail.net # reset a PhoneMail password
bash scripts/mem.sh # memory per process
Scheduled PostgreSQL backups, log rotation limits for the containers, and monitoring/alerting are still to do (see Roadmap).