171 lines
6.9 KiB
YAML
171 lines
6.9 KiB
YAML
name: usesend
|
|
networks:
|
|
nginx-bridge:
|
|
external: true
|
|
services:
|
|
redis:
|
|
image: redis:7
|
|
container_name: usesend-redis
|
|
hostname: usesend-redis
|
|
networks:
|
|
- nginx-bridge
|
|
env_file: .env
|
|
labels:
|
|
com.centurylinklabs.watchtower.enable: 'true'
|
|
volumes:
|
|
- ./volumes/redis:/data:Z
|
|
command:
|
|
- redis-server
|
|
- --maxmemory-policy
|
|
- noeviction
|
|
tty: true
|
|
stdin_open: true
|
|
restart: unless-stopped
|
|
# S3-compatible storage for images uploaded in the email editor, following
|
|
# upstream's docker/prod/compose.yml (RustFS replaced MinIO, whose images are
|
|
# no longer published). The browser uploads straight to S3_COMPATIBLE_API_URL
|
|
# and sent emails link to S3_COMPATIBLE_PUBLIC_URL, so NPM proxies
|
|
# storage.usesend.gbrown.org -> http://usesend-storage:9000. No host ports:
|
|
# the console (9001) is only reachable from inside nginx-bridge.
|
|
# Pinned like upstream. Bump the tag deliberately when upstream does.
|
|
rustfs:
|
|
image: docker.io/rustfs/rustfs:1.0.4
|
|
container_name: usesend-storage
|
|
hostname: usesend-storage
|
|
networks:
|
|
- nginx-bridge
|
|
# RustFS runs as uid 10001; keep-id keeps ./volumes/rustfs gib-owned (AGENTS.md §3.2)
|
|
userns_mode: keep-id:uid=10001,gid=10001
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
environment:
|
|
RUSTFS_ACCESS_KEY: ${RUSTFS_ACCESS_KEY:?err}
|
|
RUSTFS_SECRET_KEY: ${RUSTFS_SECRET_KEY:?err}
|
|
RUSTFS_ADDRESS: 0.0.0.0:9000
|
|
RUSTFS_CONSOLE_ADDRESS: 0.0.0.0:9001
|
|
RUSTFS_CONSOLE_ENABLE: 'true'
|
|
RUSTFS_OBS_LOGGER_LEVEL: error
|
|
TZ: America/New_York
|
|
volumes:
|
|
- ./volumes/rustfs:/data:Z
|
|
command: ["/data"]
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:9000/health"]
|
|
interval: 20s
|
|
timeout: 10s
|
|
retries: 5
|
|
start_period: 40s
|
|
restart: unless-stopped
|
|
# One-shot: creates the bucket, allows browser uploads from the app (CORS) and
|
|
# makes objects publicly readable so images load in sent emails. Reruns on
|
|
# every unit start, which is harmless.
|
|
rustfs-init:
|
|
image: docker.io/amazon/aws-cli:2.34.30
|
|
container_name: usesend-storage-init
|
|
networks:
|
|
- nginx-bridge
|
|
entrypoint: ["/bin/sh", "-ec"]
|
|
environment:
|
|
AWS_ACCESS_KEY_ID: ${RUSTFS_ACCESS_KEY:?err}
|
|
AWS_SECRET_ACCESS_KEY: ${RUSTFS_SECRET_KEY:?err}
|
|
AWS_DEFAULT_REGION: us-east-1
|
|
AWS_EC2_METADATA_DISABLED: 'true'
|
|
BUCKET: ${S3_COMPATIBLE_BUCKET:-unsend}
|
|
APP_ORIGIN: ${USESEND_URL:?err}
|
|
depends_on:
|
|
rustfs:
|
|
condition: service_healthy
|
|
command:
|
|
- |
|
|
s3="aws --endpoint-url http://usesend-storage:9000 s3api"
|
|
$$s3 head-bucket --bucket "$$BUCKET" >/dev/null 2>&1 || $$s3 create-bucket --bucket "$$BUCKET"
|
|
$$s3 put-bucket-cors --bucket "$$BUCKET" --cors-configuration \
|
|
'{"CORSRules":[{"AllowedOrigins":["'"$$APP_ORIGIN"'"],"AllowedMethods":["GET","HEAD","PUT"],"AllowedHeaders":["*"],"ExposeHeaders":["ETag"],"MaxAgeSeconds":3600}]}'
|
|
$$s3 put-bucket-policy --bucket "$$BUCKET" --policy \
|
|
'{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::'"$$BUCKET"'/*"]}]}'
|
|
restart: 'no'
|
|
smtp-server:
|
|
image: usesend/smtp-proxy:latest
|
|
container_name: usesend-smtp-server
|
|
hostname: usesend-smtp
|
|
networks:
|
|
nginx-bridge:
|
|
# The proxy REQUIRES STARTTLS before it will accept AUTH, so clients must
|
|
# reach it by a name the certificate actually covers: smtp.usesend.gbrown.org,
|
|
# validated by the *.usesend.gbrown.org SAN on npm-43 (see certs/README.md).
|
|
#
|
|
# Resolution: podman's aardvark-dns answers this name authoritatively for
|
|
# containers ON nginx-bridge, handing back the container IP. NPM is not
|
|
# involved at all -- it only reverse-proxies HTTP(S). Cloudflare is not
|
|
# involved either: there is deliberately no public DNS record for this name,
|
|
# so if the container is stopped, lookups fail fast with NXDOMAIN.
|
|
aliases:
|
|
- smtp.usesend.gbrown.org
|
|
env_file: .env
|
|
environment:
|
|
- SMTP_AUTH_USERNAME=${SMTP_USER:?err}
|
|
- USESEND_BASE_URL=${USESEND_URL:?err}
|
|
# The image's built-in default cert cannot complete a TLS 1.3 handshake at all
|
|
# ("no suitable signature algorithm"), which silently made STARTTLS -- and so
|
|
# ALL inbound SMTP -- impossible. These point it at the real wildcard cert.
|
|
- USESEND_API_CERT_PATH=/certs/fullchain.pem
|
|
- USESEND_API_KEY_PATH=/certs/privkey.pem
|
|
ports:
|
|
# Bound to the WireGuard address ONLY, never 0.0.0.0 (AGENTS.md §13.2). This is an
|
|
# authenticated relay; putting it on the public interface would invite the internet to
|
|
# brute-force it, and contradicts the Cloudflare-only posture in §7.
|
|
# The `Home` wg interface sits in firewalld's `trusted` zone, so no rule is needed.
|
|
# Rootless can bind these because net.ipv4.ip_unprivileged_port_start = 80.
|
|
# Consumers on the home server reach it as smtp.usesend.gbrown.org (a UniFi DNS record
|
|
# maps that name to 192.168.2.2) so the cert still validates by hostname.
|
|
- "192.168.2.2:587:587"
|
|
- "192.168.2.2:465:465"
|
|
volumes:
|
|
# Copied from NPM's letsencrypt store, NOT bind-mounted from it: NPM mounts that
|
|
# tree with :Z (private SELinux category), so a second consumer would fight it.
|
|
# TRADE-OFF: copies go stale. Refresh them when NPM renews (see certs/README).
|
|
- ./certs:/certs:ro,Z
|
|
labels:
|
|
com.centurylinklabs.watchtower.enable: 'true'
|
|
tty: true
|
|
stdin_open: true
|
|
restart: unless-stopped
|
|
usesend:
|
|
image: usesend/usesend:latest
|
|
container_name: usesend
|
|
hostname: usesend
|
|
domainname: ${USESEND_URL:?err}
|
|
networks:
|
|
- nginx-bridge
|
|
env_file: .env
|
|
environment:
|
|
- COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
|
- PORT=${USESEND_PORT:-3000}
|
|
- DATABASE_URL=${DATABASE_URL:?err}
|
|
- NEXTAUTH_URL=${USESEND_URL:?err}
|
|
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err}
|
|
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:?err}
|
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:?err}
|
|
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:?err}
|
|
- GITHUB_ID=${GITHUB_ID:?err}
|
|
- GITHUB_SECRET=${GITHUB_SECRET:?err}
|
|
- REDIS_URL=${REDIS_URL:?err}
|
|
- NEXT_PUBLIC_IS_CLOUD=${NEXT_PUBLIC_IS_CLOUD:-false}
|
|
- API_RATE_LIMIT=${API_RATE_LIMIT:-1}
|
|
- SMTP_HOST=${SMTP_HOST:-smtp.usesend.gbrown.org}
|
|
- SMTP_USER=${SMTP_USER:-usesend}
|
|
# Editor image uploads (the rustfs service above). Uploads are disabled if any is unset.
|
|
- S3_COMPATIBLE_ACCESS_KEY=${RUSTFS_ACCESS_KEY:?err}
|
|
- S3_COMPATIBLE_SECRET_KEY=${RUSTFS_SECRET_KEY:?err}
|
|
- S3_COMPATIBLE_API_URL=${S3_COMPATIBLE_API_URL:?err}
|
|
- S3_COMPATIBLE_PUBLIC_URL=${S3_COMPATIBLE_PUBLIC_URL:?err}
|
|
- S3_COMPATIBLE_BUCKET=${S3_COMPATIBLE_BUCKET:-unsend}
|
|
labels:
|
|
com.centurylinklabs.watchtower.enable: 'true'
|
|
depends_on:
|
|
redis:
|
|
condition: service_started
|
|
tty: false
|
|
stdin_open: false
|
|
restart: unless-stopped
|