Skip to main content

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​

ItemLocation
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
CertificatesLet'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​

RecordValue
A @, CNAME wwwthe home IP; www redirects to phonemail.net
A mailthe home IP
MX @mail.phonemail.net, priority 10
TXT @v=spf1 mx ~all
TXT _dmarcv=DMARC1; p=quarantine; adkim=r; aspf=r
TXT mail._domainkeythe 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)​

PathGoes 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 elsethe 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 get 550 during the SMTP conversation and no bounces are sent to forged senders. Accepted mail goes to the mail service on 127.0.0.1:2525, which parses MIME (multipart, quoted-printable, base64, charsets, HTML to text), threads by In-Reply-To, and stores a message once per Message-ID.
  • Outgoing: messages with outside recipients get an outbox row. 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 phonemail plugin sends logins of the form <10 digits>@phonemail.net to PhoneMail's IMAP, submission and password ports, which listen only on the Docker bridge address.

Settings (.env)​

VariableMeaning
POSTGRES_PASSWORD, INTERNAL_TOKEN, JWT_SECRETGenerated once per machine; never committed
MAIL_DOMAINphonemail.net
PUBLIC_BASE_URLhttps://mail.phonemail.net (used to check Twilio signatures)
API_BIND127.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_ALIASESNames of ordinary mailboxes on the same server, which PhoneMail must not take
SMTP_RELAY172.28.0.1:25, the host's Postfix
WEBMAIL_BIND172.17.0.1, where IMAP, submission and the password endpoint listen for Roundcube
TWILIO_FROM_NUMBER+15078000027
TWILIO_VERIFY_SIDTwilio Verify service; empty = OTP off, password login shown
SMS_ALERTS, SMS_ALERT_BODYNew-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
Not set up yet

Scheduled PostgreSQL backups, log rotation limits for the containers, and monitoring/alerting are still to do (see Roadmap).