From 0f1e568009f1208883be80ee1557ab023f78e392 Mon Sep 17 00:00:00 2001 From: Agent Date: Thu, 1 Oct 2026 16:23:10 -0400 Subject: [PATCH 1/3] Add Deployment section to README Document production deployment including: - Multi-stage Docker image build process (build + production stages) - Docker Compose setup with env substitution and restart policy - Required production environment variables with notes - Persistent SQLite data volume management, backup and migration --- README.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/README.md b/README.md index d07fedc..71875ad 100644 --- a/README.md +++ b/README.md @@ -176,3 +176,108 @@ pnpm test:unit # Run unit tests (vitest) pnpm test # Run integration tests (bash E2E) pnpm test:server # Start server for manual testing ``` + +## Deployment + +### Production Image Build + +Mailship uses a **multi-stage Docker build** to produce a minimal production image. + +| Stage | Base Image | Purpose | +|---|---|---| +| `build` | `node:22-alpine` | Compile TypeScript, install all dependencies | +| `production` | `node:22-alpine` | Runtime — only production deps, compiled JS, static assets | + +The Dockerfile is structured to keep the final image small by discarding build tooling +(`python3`, `make`, `g++`, `git`) and development dependencies after compilation. + +```sh +docker build -t mailship . +``` + +The image listens on **port 4738**, expects a writable **`/data`** volume for the SQLite +database, and runs as a non-root user (`app`) for security hardening. + +### Docker Compose (Recommended) + +Copy the environment template, fill in your secrets, and start: + +```sh +cp .env.template .env +# Edit .env with your production values (see Required Environment Variables below) +docker compose up -d +``` + +The compose file (`docker-compose.yml`) configures: + +- **Automatic restarts** via `restart: unless-stopped` +- **Named volume** `mailship-data` mounted at `/data` for database persistence +- **Port 4738** published to the host +- **Environment variables** passed from your `.env` file with `.env` substitution +- **Required vars** (`MAILSHIP_SECRET`, `MAILSHIP_URL`, `BASE_URL`, `SMTP_*`) — the + compose file uses `${VAR:?required}` to fail early if any are missing + +To pull the latest image and recreate the container after a rebuild: + +```sh +docker compose build --pull +# or if using a pre-built image: +docker compose pull +docker compose up -d +``` + +### Required Environment Variables + +The table below marks variables that are especially important in **production**. +Defaults are safe for development but may leak information or misroute mail in a +production deployment. + +| Variable | Required in production | Notes | +|---|---|---| +| `MAILSHIP_SECRET` | ✓ | Must be a **private key in hex** — never share or commit this value | +| `MAILSHIP_URL` | ✓ | Set to the **public-facing URL** of your instance (e.g. `https://mail.example.com`) | +| `BASE_URL` | ✓ | Same as `MAILSHIP_URL` in most setups; used for callback URLs in email | +| `SMTP_HOST` | ✓ | Production SMTP server (e.g. `smtp.sendgrid.net`, `email-smtp.us-east-1.amazonaws.com`) | +| `SMTP_PORT` | ✓ | 587 (STARTTLS) or 465 (TLS); verify with your provider | +| `SMTP_USER` | ✓ | SMTP authentication username | +| `SMTP_PASSWORD` | ✓ | SMTP authentication password — use a secret manager or env injection, not a file | +| `SMTP_FROM` | ✓ | The `From:` address recipients will see (e.g. `"Mailship" `) | +| `DEFAULT_RELAYS` | | Comma-separated relay list; production should tune these to reliable relays | +| `INDEXER_RELAYS` | | Same — use relays you trust for discovery | +| `SEARCH_RELAYS` | | Same — use a relay that supports NIP-50 search | +| `CORS_ORIGIN` | ✓ | **Must** be set to the exact origin your browser client runs on (e.g. `https://app.example.com`). The server refuses to start without it. | +| `DATA_DIR` | | Default `/data` in the Docker image; change only if you mount a different volume path | +| `BRAND_NAME`, `BRAND_ACCENT`, `BRAND_LOGO` | | Branding overrides — safe to leave at defaults | +| `EVENT_VIEWER_URL` | | Controls where event links in emails point; defaults to Flotilla, set to your app's URL | +| `PORT` | | Internal port (default `4738`) — no need to change unless remapping in compose | + +> **Secrets in production:** Never commit `.env` or embed secrets in the compose +> file. Use your infrastructure's secret store (e.g. Docker secrets, HashiCorp Vault, +> Ansible Vault, 1Password CLI) and inject variables at deploy time. + +### Persistent Data Volume + +Mailship stores its data in a **SQLite database** located at `$DATA_DIR/mailship.db` +(`/data/mailship.db` by default in the Docker image). + +- **The database is the single source of truth** for subscriptions, events, and + delivery state. Losing it means losing all subscriptions and digest history. +- **Backup regularly** — a simple `sqlite3 /data/mailship.db .dump` or file-level + copy (while the service is idle) is sufficient. For zero-downtime backups, use + SQLite's `.backup` command or VACUUM INTO. +- **Do not share the volume** across multiple Mailship instances — the in-process + cron scheduler assumes a single writer. +- **Volume location** when using Docker Compose: + +```sh +docker volume inspect mailship_mailship-data # inspect mount point +docker run --rm -v mailship_mailship-data:/data alpine ls -la /data # browse contents +``` + +To migrate or snapshot, stop the container and copy the database file: + +```sh +docker compose down +docker run --rm -v mailship_mailship-data:/data -v $(pwd):/backup alpine cp /data/mailship.db /backup/mailship-$(date +%F).db +docker compose up -d +``` -- 2.45.2 From e6b48796ffafa21aa8455b2bc9fab6cc049478e5 Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 1 Oct 2026 20:29:15 +0000 Subject: [PATCH 2/3] Pare down Deployment instructions --- README.md | 98 ++----------------------------------------------------- 1 file changed, 2 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 71875ad..8647ebf 100644 --- a/README.md +++ b/README.md @@ -148,48 +148,10 @@ pnpm run preview:digest open digest-preview.html ``` -## Docker - -### Quick start - -```sh -cp .env.template .env -# Fill in your secrets in .env -docker compose up -d -``` - -### Build and run manually - -```sh -docker build -t mailship . -docker run -d \ - -p 4738:4738 \ - -v mailship-data:/data \ - --env-file .env \ - mailship -``` - -## Tests - -```sh -pnpm test:unit # Run unit tests (vitest) -pnpm test # Run integration tests (bash E2E) -pnpm test:server # Start server for manual testing -``` ## Deployment -### Production Image Build - -Mailship uses a **multi-stage Docker build** to produce a minimal production image. - -| Stage | Base Image | Purpose | -|---|---|---| -| `build` | `node:22-alpine` | Compile TypeScript, install all dependencies | -| `production` | `node:22-alpine` | Runtime — only production deps, compiled JS, static assets | - -The Dockerfile is structured to keep the final image small by discarding build tooling -(`python3`, `make`, `g++`, `git`) and development dependencies after compilation. +The recommended deployment method for mailship is using Docker. You can build the docker image from the repo root with: ```sh docker build -t mailship . @@ -204,7 +166,7 @@ Copy the environment template, fill in your secrets, and start: ```sh cp .env.template .env -# Edit .env with your production values (see Required Environment Variables below) +# Edit .env with your production values (see Configuration section in README) docker compose up -d ``` @@ -225,59 +187,3 @@ docker compose build --pull docker compose pull docker compose up -d ``` - -### Required Environment Variables - -The table below marks variables that are especially important in **production**. -Defaults are safe for development but may leak information or misroute mail in a -production deployment. - -| Variable | Required in production | Notes | -|---|---|---| -| `MAILSHIP_SECRET` | ✓ | Must be a **private key in hex** — never share or commit this value | -| `MAILSHIP_URL` | ✓ | Set to the **public-facing URL** of your instance (e.g. `https://mail.example.com`) | -| `BASE_URL` | ✓ | Same as `MAILSHIP_URL` in most setups; used for callback URLs in email | -| `SMTP_HOST` | ✓ | Production SMTP server (e.g. `smtp.sendgrid.net`, `email-smtp.us-east-1.amazonaws.com`) | -| `SMTP_PORT` | ✓ | 587 (STARTTLS) or 465 (TLS); verify with your provider | -| `SMTP_USER` | ✓ | SMTP authentication username | -| `SMTP_PASSWORD` | ✓ | SMTP authentication password — use a secret manager or env injection, not a file | -| `SMTP_FROM` | ✓ | The `From:` address recipients will see (e.g. `"Mailship" `) | -| `DEFAULT_RELAYS` | | Comma-separated relay list; production should tune these to reliable relays | -| `INDEXER_RELAYS` | | Same — use relays you trust for discovery | -| `SEARCH_RELAYS` | | Same — use a relay that supports NIP-50 search | -| `CORS_ORIGIN` | ✓ | **Must** be set to the exact origin your browser client runs on (e.g. `https://app.example.com`). The server refuses to start without it. | -| `DATA_DIR` | | Default `/data` in the Docker image; change only if you mount a different volume path | -| `BRAND_NAME`, `BRAND_ACCENT`, `BRAND_LOGO` | | Branding overrides — safe to leave at defaults | -| `EVENT_VIEWER_URL` | | Controls where event links in emails point; defaults to Flotilla, set to your app's URL | -| `PORT` | | Internal port (default `4738`) — no need to change unless remapping in compose | - -> **Secrets in production:** Never commit `.env` or embed secrets in the compose -> file. Use your infrastructure's secret store (e.g. Docker secrets, HashiCorp Vault, -> Ansible Vault, 1Password CLI) and inject variables at deploy time. - -### Persistent Data Volume - -Mailship stores its data in a **SQLite database** located at `$DATA_DIR/mailship.db` -(`/data/mailship.db` by default in the Docker image). - -- **The database is the single source of truth** for subscriptions, events, and - delivery state. Losing it means losing all subscriptions and digest history. -- **Backup regularly** — a simple `sqlite3 /data/mailship.db .dump` or file-level - copy (while the service is idle) is sufficient. For zero-downtime backups, use - SQLite's `.backup` command or VACUUM INTO. -- **Do not share the volume** across multiple Mailship instances — the in-process - cron scheduler assumes a single writer. -- **Volume location** when using Docker Compose: - -```sh -docker volume inspect mailship_mailship-data # inspect mount point -docker run --rm -v mailship_mailship-data:/data alpine ls -la /data # browse contents -``` - -To migrate or snapshot, stop the container and copy the database file: - -```sh -docker compose down -docker run --rm -v mailship_mailship-data:/data -v $(pwd):/backup alpine cp /data/mailship.db /backup/mailship-$(date +%F).db -docker compose up -d -``` -- 2.45.2 From 212edabc5d058fe90a06771d6e2b8538a528548a Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 1 Oct 2026 20:32:57 +0000 Subject: [PATCH 3/3] More deployment section updates --- README.md | 70 ++++++++++++++++++++++++------------------------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 8647ebf..a2103ca 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,36 @@ Flotilla ──HTTP──▶ Mailship (PUT /subscription/email) cron fires ──▶ render digest ──▶ SMTP ──▶ email ``` +## Deployment + +The recommended deployment method for mailship is via Docker. Currently the image is not published anywhere. You can build it locally from the repo root with: + +```sh +docker build -t mailship . +``` + +The image listens on **port 4738**, expects a writable **`/data`** volume for the SQLite +database, and runs as a non-root user (`app`) for security hardening. + +### Docker Compose (Recommended) + +To deploy using Docker Compose copy the environment template, configure your variables, and start: + +```sh +cp .env.template .env +# Edit .env with your production values (see Configuration section in README) +docker compose up -d --build +``` + +The compose file (`docker-compose.yml`) configures: + +- **Automatic restarts** via `restart: unless-stopped` +- **Named volume** `mailship-data` mounted at `/data` for database persistence +- **Port 4738** published to the host +- **Environment variables** passed from your `.env` file with `.env` substitution +- **Required vars** (`MAILSHIP_SECRET`, `MAILSHIP_URL`, `BASE_URL`, `SMTP_*`) — the + compose file uses `${VAR:?required}` to fail early if any are missing + ## Configuration | Variable | Required | Description | @@ -147,43 +177,3 @@ result in your browser: pnpm run preview:digest open digest-preview.html ``` - - -## Deployment - -The recommended deployment method for mailship is using Docker. You can build the docker image from the repo root with: - -```sh -docker build -t mailship . -``` - -The image listens on **port 4738**, expects a writable **`/data`** volume for the SQLite -database, and runs as a non-root user (`app`) for security hardening. - -### Docker Compose (Recommended) - -Copy the environment template, fill in your secrets, and start: - -```sh -cp .env.template .env -# Edit .env with your production values (see Configuration section in README) -docker compose up -d -``` - -The compose file (`docker-compose.yml`) configures: - -- **Automatic restarts** via `restart: unless-stopped` -- **Named volume** `mailship-data` mounted at `/data` for database persistence -- **Port 4738** published to the host -- **Environment variables** passed from your `.env` file with `.env` substitution -- **Required vars** (`MAILSHIP_SECRET`, `MAILSHIP_URL`, `BASE_URL`, `SMTP_*`) — the - compose file uses `${VAR:?required}` to fail early if any are missing - -To pull the latest image and recreate the container after a rebuild: - -```sh -docker compose build --pull -# or if using a pre-built image: -docker compose pull -docker compose up -d -``` -- 2.45.2