Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Docker Compose configuration for self-hosting Shroud.email.

Please read our [deployment documentation](https://shroud.email/docs/deployment/self-host) on our website.

If you just want to get up and running with Shroud.email quickly, you can sign up for our hosted version [here](https://app.shroud.email/users/register).
If you want to get up and running with Shroud.email quickly, and don't want to maintain your own mailserver, you can sign up for our hosted version [here](https://app.shroud.email/users/register).

Copy `haraka/haraka_config/config/me.example` to `haraka/haraka_config/config/me` and set your mail hostname.

Expand All @@ -24,7 +24,7 @@ leave the defaults get HTTP-01 and never need a Bunny key.

## Living on the edge

The committed `docker-compose.yaml` tracks the stable `:1` image. If you'd rather
The committed `docker-compose.yaml` tracks the stable image. If you'd rather
run the latest `:edge` build (rebuilt on every push to `main`) and have it
auto-update, copy the example override and bring the stack up:

Expand All @@ -35,4 +35,36 @@ docker compose up -d

This points the `web` service at `:edge` and adds [Watchtower](https://containrrr.dev/watchtower/),
which polls every 5 minutes and auto-recreates `web` (and only `web`) when a new
image is published.
image is published.

## Cap CAPTCHA

The compose file includes a [Cap](https://trycap.dev) self-hosted CAPTCHA
instance. It is **opt-in at the application level**: the
services run by default, but the widget is not rendered and verification
is not performed until you set all three `CAP_*` variables on the `web`
service.

> **Public ingress required.** `CAP_INSTANCE_URL` must be a URL a user's
> browser can reach over HTTPS.

### Setup

1. Generate an admin key and set `CAP_ADMIN_KEY` in `.env`:
```bash
openssl rand -hex 32
```

2. Start the services:
```bash
docker compose up -d cap valkey
Comment thread
taobojlen marked this conversation as resolved.
```

3. Create a site key. Cap authenticates with a
session token issued by logging in with the `ADMIN_KEY. Create a `siteKey` and `secretKey` in the Cap UI.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

4. Set `CAP_INSTANCE_URL`, `CAP_SITE_KEY`, and `CAP_SECRET_KEY` in `.env`, then
restart `web`:
```bash
docker compose restart web
```
7 changes: 7 additions & 0 deletions caddy/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
}
}

{$CAP_DOMAIN:disabled.localhost} {
reverse_proxy cap:3000
tls {
issuer acme
}
}

{$EMAIL_DOMAIN} {
tls {
issuer acme
Expand Down
7 changes: 7 additions & 0 deletions caddy/Caddyfile.bunny
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
}
}

{$CAP_DOMAIN:disabled.localhost} {
reverse_proxy cap:3000
tls {
dns bunny {env.BUNNY_API_KEY}
}
}

{$EMAIL_DOMAIN} {
tls {
issuer acme
Expand Down
22 changes: 22 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ services:
- ./haraka/haraka_config:/app/haraka_config
- pem_certs:/app/haraka_config/config/certs

cap:
image: tiago2/cap:3
restart: unless-stopped
depends_on:
- valkey
environment:
- ADMIN_KEY=${CAP_ADMIN_KEY}
- REDIS_URL=redis://valkey:6379
- CORS_ORIGIN=https://${APP_DOMAIN}

valkey:
image: valkey/valkey:9-alpine
restart: unless-stopped
command: valkey-server --save 60 1 --loglevel warning --maxmemory 256mb --maxmemory-policy noeviction
volumes:
- valkey_data:/data

web:
image: ghcr.io/shroud-email/shroud.email:1
restart: unless-stopped
Expand Down Expand Up @@ -76,6 +93,9 @@ services:
- LOOPS_ACTIVE_USERS_LIST_ID=${LOOPS_ACTIVE_USERS_LIST_ID}
- CHATWOOT_MAC_TOKEN=${CHATWOOT_MAC_TOKEN}
- CHATWOOT_BASE_URL=${CHATWOOT_BASE_URL}
- CAP_INSTANCE_URL=${CAP_INSTANCE_URL:-https://${CAP_DOMAIN}}
- CAP_SITE_KEY=${CAP_SITE_KEY}
- CAP_SECRET_KEY=${CAP_SECRET_KEY}

caddy:
build: ./caddy
Expand All @@ -85,6 +105,7 @@ services:
- APP_DOMAIN=${APP_DOMAIN}
- EMAIL_DOMAIN=${EMAIL_DOMAIN}
- BUNNY_API_KEY=${BUNNY_API_KEY}
- CAP_DOMAIN=${CAP_DOMAIN}
ports:
- "80:80"
- "443:443"
Expand All @@ -105,3 +126,4 @@ volumes:
db_data:
caddy_data:
pem_certs:
valkey_data:
31 changes: 31 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,34 @@ CADDYFILE_PATH=./caddy/Caddyfile

DB_USER=postgres
DB_DATABASE=shroud

## Cap CAPTCHA (optional but included in the default compose).
## Set all three to enable Cap on the signup/login/reset forms.
## CAP_ADMIN_KEY: dashboard password. Generate with: openssl rand -hex 32
CAP_ADMIN_KEY=
## CAP_DOMAIN: the public hostname Caddy will serve the Cap instance on
## (e.g. cap.shroud.email). Required to enable Cap — create a DNS A-record
## for this host pointing at the server, the same as APP_DOMAIN.
## Caddy auto-provisions TLS for it (HTTP-01 by default, or Bunny DNS-01 if
## you set CADDYFILE_PATH=./caddy/Caddyfile.bunny). When unset, Caddy
## leaves the Cap route inert (no cert, no traffic) and Cap stays disabled.
CAP_DOMAIN=
## CAP_INSTANCE_URL: the PUBLIC, browser-reachable HTTPS URL of your Cap
## instance. Defaults to https://${CAP_DOMAIN}. The widget renders this into
## data-cap-api-endpoint, so a user's browser must be able to reach it over
## HTTPS (http:// will be blocked as mixed content on your https://APP_DOMAIN
## pages). Override only if you front Cap with a path-based or off-domain URL.
CAP_INSTANCE_URL=
## Create a site key (rsw + instrumentation) by logging in with ADMIN_KEY
## first (Cap's Bot scheme is for API keys, not the admin key):
## RESP=$(curl -s -X POST http://localhost:3000/auth/login \
## -H "Content-Type: application/json" \
## -d "{\"admin_key\":\"$CAP_ADMIN_KEY\"}")
## BEARER=$(printf '{"token":"%s","hash":"%s"}' \
## $(echo "$RESP" | jq -r .session_token) \
## $(echo "$RESP" | jq -r .hashed_token) | base64 -w0)
## curl -X POST http://localhost:3000/server/keys \
## -H "Authorization: Bearer $BEARER" \
## -d '{"name":"shroud-email","instrumentation":true,"rsw":true}'
CAP_SITE_KEY=
CAP_SECRET_KEY=
Loading