elektrine/deploy/docker/compose.full.yml
maxfield 1a7be85be9
All checks were successful
Deploy Docker Images / Build, push, and deploy (push) Successful in 1m19s
fix(deploy): give BEAM entrypoint caps and Caddy access to Bluesky
Restore CHOWN/SETUID/SETGID/etc. after cap_drop ALL so containers boot.
Attach bluesky_pds to the proxy network so Caddy can resolve it.
2026-08-02 23:25:30 -04:00

444 lines
16 KiB
YAML

# Shared security for BEAM roles that do not need host networking / NET_ADMIN.
# Keep a small cap set: entrypoint runs as root to chown /data, su to nobody,
# and symlink uploads under /app. cap_drop ALL with no adds crashes the boot.
x-beam-security: &beam_security
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:size=64M,mode=1777
services:
postgres:
image: pgvector/pgvector:pg16@sha256:00ba258a66dac104fd5171074a0084462a64a1369d8513f3d0a634e2f24d15bc
container_name: elektrine_db
shm_size: ${POSTGRES_SHM_SIZE:-512m}
command:
[
"postgres",
"-c",
"shared_preload_libraries=pg_stat_statements",
"-c",
"pg_stat_statements.max=10000",
"-c",
"pg_stat_statements.track=all"
]
environment:
POSTGRES_USER: elektrine
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD}
POSTGRES_DB: elektrine_prod
volumes:
- postgres_data:/var/lib/postgresql/data
- ./initdb:/docker-entrypoint-initdb.d:ro
ports:
- "${DB_BIND:-127.0.0.1:5432:5432}"
# Backend network only — Caddy never joins this network.
networks:
- elektrine_network
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U elektrine -d elektrine_prod"]
interval: 10s
timeout: 5s
retries: 5
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
security_opt:
- no-new-privileges:true
app:
<<: *beam_security
image: ${ELEKTRINE_IMAGE:-elektrine:local}
build:
context: ../..
dockerfile: deploy/docker/Dockerfile
args:
RELEASE_NAME: ${RELEASE_NAME:-elektrine}
ELEKTRINE_RELEASE_MODULES: ${ELEKTRINE_RELEASE_MODULES:-all}
INSTALL_TOR: ${INSTALL_TOR:-auto}
INSTALL_VPN_TOOLS: ${INSTALL_VPN_TOOLS:-auto}
PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:-example.com}
EMAIL_DOMAIN: ${EMAIL_DOMAIN:-${PRIMARY_DOMAIN:-example.com}}
SUPPORTED_DOMAINS: ${SUPPORTED_DOMAINS:-${PRIMARY_DOMAIN:-example.com}}
PROFILE_BASE_DOMAINS: ${PROFILE_BASE_DOMAINS:-${PRIMARY_DOMAIN:-example.com}}
container_name: elektrine_app
depends_on:
postgres:
condition: service_healthy
env_file:
- ../../.env.production
environment:
DATABASE_URL: ecto://elektrine:${DB_PASSWORD:?set DB_PASSWORD}@postgres/elektrine_prod
POOL_SIZE: ${APP_POOL_SIZE:-10}
PHX_HOST: ${PHX_HOST:-example.com}
CADDY_ADMIN_HOST: ${CADDY_ADMIN_HOST:-admin.elektrine.com}
PHX_SERVER: "true"
PORT: ${PORT:-8080}
PHX_HTTP_IP: ${DOCKER_PHX_HTTP_IP:-0.0.0.0}
TRUSTED_PROXY_CIDRS: ${TRUSTED_PROXY_CIDRS:-172.30.0.0/24}
CADDY_ASK_TRUSTED_CIDRS: ${CADDY_ASK_TRUSTED_CIDRS:-${TRUSTED_PROXY_CIDRS:-172.30.0.0/24}}
MIX_ENV: prod
ELEKTRINE_RUNTIME_ROLE: app
ELEKTRINE_ENABLED_MODULES: ${ELEKTRINE_ENABLED_MODULES:-all}
ELEKTRINE_ENABLE_MAIL: "false"
ELEKTRINE_ENABLE_TOR: ${ELEKTRINE_ENABLE_TOR:-false}
TURN_ENABLED: ${TURN_ENABLED:-false}
ONION_TLS_ENABLED: ${ONION_TLS_ENABLED:-true}
MIGRATION_POOL_SIZE: ${MIGRATION_POOL_SIZE:-2}
ports:
- "${APP_HTTP_BIND:-127.0.0.1:8080:8080}"
volumes:
- app_data:/data
- uploads:/data/uploads
networks:
elektrine_network:
caddy_proxy_network:
aliases:
- app_proxy
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:$${PORT:-8080}/health >/dev/null || curl -g -fsS http://[::1]:$${PORT:-8080}/health >/dev/null || exit 1"]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
restart: unless-stopped
worker:
<<: *beam_security
image: ${ELEKTRINE_IMAGE:-elektrine:local}
build:
context: ../..
dockerfile: deploy/docker/Dockerfile
args:
RELEASE_NAME: ${RELEASE_NAME:-elektrine}
ELEKTRINE_RELEASE_MODULES: ${ELEKTRINE_RELEASE_MODULES:-all}
INSTALL_TOR: ${INSTALL_TOR:-auto}
INSTALL_VPN_TOOLS: ${INSTALL_VPN_TOOLS:-auto}
PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:-example.com}
EMAIL_DOMAIN: ${EMAIL_DOMAIN:-${PRIMARY_DOMAIN:-example.com}}
SUPPORTED_DOMAINS: ${SUPPORTED_DOMAINS:-${PRIMARY_DOMAIN:-example.com}}
PROFILE_BASE_DOMAINS: ${PROFILE_BASE_DOMAINS:-${PRIMARY_DOMAIN:-example.com}}
container_name: elektrine_worker
depends_on:
postgres:
condition: service_healthy
env_file:
- ../../.env.production
environment:
DATABASE_URL: ecto://elektrine:${DB_PASSWORD:?set DB_PASSWORD}@postgres/elektrine_prod
POOL_SIZE: ${WORKER_POOL_SIZE:-5}
PHX_HOST: ${PHX_HOST:-example.com}
MIX_ENV: prod
ELEKTRINE_RUNTIME_ROLE: worker
ELEKTRINE_ENABLED_MODULES: ${ELEKTRINE_ENABLED_MODULES:-all}
ELEKTRINE_ENABLE_MAIL: "false"
ELEKTRINE_ENABLE_TOR: "false"
TURN_ENABLED: ${TURN_ENABLED:-false}
MIGRATION_POOL_SIZE: ${MIGRATION_POOL_SIZE:-2}
ACME_HOME: ${ACME_HOME:-/data/acme.sh}
ACME_SH_BIN: ${ACME_SH_BIN:-/data/acme.sh/acme.sh}
ACME_WILDCARD_RENEWAL_ENABLED: ${ACME_WILDCARD_RENEWAL_ENABLED:-false}
ELEKTRINE_API_BASE: ${ELEKTRINE_API_BASE:-http://app:8080}
CADDY_TLS_MOUNT_DIR: ${CADDY_TLS_MOUNT_DIR:-/opt/elektrine/certs}
RELOAD_CMD: ${ACME_RELOAD_CMD:-true}
volumes:
- app_data:/data
- uploads:/data/uploads
- ${CADDY_TLS_MOUNT_DIR:-/opt/elektrine/certs}:/opt/elektrine/certs
networks:
- elektrine_network
restart: unless-stopped
vpn:
profiles: ["vpn"]
image: ${ELEKTRINE_IMAGE:-elektrine:local}
build:
context: ../..
dockerfile: deploy/docker/Dockerfile
args:
RELEASE_NAME: ${RELEASE_NAME:-elektrine}
ELEKTRINE_RELEASE_MODULES: ${ELEKTRINE_RELEASE_MODULES:-all}
INSTALL_TOR: ${INSTALL_TOR:-auto}
INSTALL_VPN_TOOLS: ${INSTALL_VPN_TOOLS:-auto}
PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:-example.com}
EMAIL_DOMAIN: ${EMAIL_DOMAIN:-${PRIMARY_DOMAIN:-example.com}}
SUPPORTED_DOMAINS: ${SUPPORTED_DOMAINS:-${PRIMARY_DOMAIN:-example.com}}
PROFILE_BASE_DOMAINS: ${PROFILE_BASE_DOMAINS:-${PRIMARY_DOMAIN:-example.com}}
container_name: elektrine_vpn
depends_on:
postgres:
condition: service_healthy
env_file:
- ../../.env.production
environment:
DATABASE_URL: ecto://elektrine:${DB_PASSWORD:?set DB_PASSWORD}@127.0.0.1/elektrine_prod
POOL_SIZE: ${VPN_POOL_SIZE:-2}
MIX_ENV: prod
ELEKTRINE_RUNTIME_ROLE: vpn
ELEKTRINE_ENABLED_MODULES: ${ELEKTRINE_ENABLED_MODULES:-all}
ELEKTRINE_ENABLE_WEB: "false"
ELEKTRINE_ENABLE_JOBS: "false"
ELEKTRINE_ENABLE_MAIL: "false"
ELEKTRINE_ENABLE_TOR: "false"
VPN_SELFHOST_NODE: "true"
VPN_SELFHOST_PROTOCOL: ${VPN_SELFHOST_PROTOCOL:-wireguard}
VPN_SELFHOST_PROTOCOLS: ${VPN_SELFHOST_PROTOCOLS:-${VPN_SELFHOST_PROTOCOL:-wireguard}}
VPN_SELFHOST_WG_INTERFACE: ${VPN_SELFHOST_WG_INTERFACE:-wg0}
VPN_SELFHOST_ADDRESS: ${VPN_SELFHOST_ADDRESS:-10.8.0.1/24}
VPN_SELFHOST_LISTEN_PORT: ${VPN_SELFHOST_LISTEN_PORT:-51820}
VPN_SELFHOST_SS_LISTEN_PORT: ${VPN_SELFHOST_SS_LISTEN_PORT:-8388}
VPN_SELFHOST_SS_PORT_RANGE_START: ${VPN_SELFHOST_SS_PORT_RANGE_START:-8388}
VPN_SELFHOST_SS_PORT_RANGE_END: ${VPN_SELFHOST_SS_PORT_RANGE_END:-9387}
VPN_SELFHOST_SS_CIPHER: ${VPN_SELFHOST_SS_CIPHER:-chacha20-ietf-poly1305}
# Unix socket ss-server pushes per-port traffic stats to for quota accounting.
VPN_SELFHOST_SS_MANAGER_SOCKET: ${VPN_SELFHOST_SS_MANAGER_SOCKET:-/data/vpn/shadowsocks/manager.sock}
network_mode: host
cap_add:
- NET_ADMIN
volumes:
- app_data:/data
- uploads:/data/uploads
restart: unless-stopped
security_opt:
- no-new-privileges:true
mail:
<<: *beam_security
profiles: ["email"]
image: ${ELEKTRINE_IMAGE:-elektrine:local}
build:
context: ../..
dockerfile: deploy/docker/Dockerfile
args:
RELEASE_NAME: ${RELEASE_NAME:-elektrine}
ELEKTRINE_RELEASE_MODULES: ${ELEKTRINE_RELEASE_MODULES:-all}
INSTALL_TOR: ${INSTALL_TOR:-auto}
INSTALL_VPN_TOOLS: ${INSTALL_VPN_TOOLS:-auto}
PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:-example.com}
EMAIL_DOMAIN: ${EMAIL_DOMAIN:-${PRIMARY_DOMAIN:-example.com}}
SUPPORTED_DOMAINS: ${SUPPORTED_DOMAINS:-${PRIMARY_DOMAIN:-example.com}}
PROFILE_BASE_DOMAINS: ${PROFILE_BASE_DOMAINS:-${PRIMARY_DOMAIN:-example.com}}
container_name: elektrine_mail
depends_on:
postgres:
condition: service_healthy
env_file:
- ../../.env.production
environment:
DATABASE_URL: ecto://elektrine:${DB_PASSWORD:?set DB_PASSWORD}@postgres/elektrine_prod
POOL_SIZE: ${MAIL_POOL_SIZE:-5}
PHX_HOST: ${PHX_HOST:-example.com}
MIX_ENV: prod
ELEKTRINE_RUNTIME_ROLE: mail
ELEKTRINE_ENABLED_MODULES: ${ELEKTRINE_ENABLED_MODULES:-all}
ELEKTRINE_ENABLE_MAIL: ${ELEKTRINE_ENABLE_MAIL:-true}
ELEKTRINE_ENABLE_TOR: "false"
SMTP_PORT: ${SMTP_PORT:-2587}
SMTPS_PORT: ${SMTPS_PORT:-2465}
IMAPS_PORT: 2993
POP3S_PORT: 2995
volumes:
- app_data:/data
- uploads:/data/uploads
- ${MAIL_TLS_MOUNT_DIR:-/opt/elektrine/certs}:/opt/elektrine/certs:ro
networks:
- elektrine_network
ports:
- "${SMTP_BIND:-127.0.0.1:587:2587}"
- "${SMTPS_BIND:-465:2465}"
- "${IMAP_BIND:-127.0.0.1:143:2143}"
- "${POP3_BIND:-127.0.0.1:110:2110}"
- "${IMAP_TLS_BIND:-993:2993}"
- "${POP3_TLS_BIND:-995:2995}"
restart: unless-stopped
# Mail listeners (SMTP/IMAP/POP3 + TLS variants) hold one file descriptor per
# connection, plus in-flight TLS handshakes. The Docker default soft nofile
# (1024) is far too low and leads to :emfile ("too many open files") storms
# under connection floods. Recreate the container for changes to take effect.
ulimits:
nofile:
soft: 65536
hard: 65536
dns:
<<: *beam_security
profiles: ["dns"]
image: ${ELEKTRINE_IMAGE:-elektrine:local}
build:
context: ../..
dockerfile: deploy/docker/Dockerfile
args:
RELEASE_NAME: ${RELEASE_NAME:-elektrine}
ELEKTRINE_RELEASE_MODULES: ${ELEKTRINE_RELEASE_MODULES:-all}
INSTALL_TOR: ${INSTALL_TOR:-auto}
INSTALL_VPN_TOOLS: ${INSTALL_VPN_TOOLS:-auto}
PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:-localhost}
EMAIL_DOMAIN: ${EMAIL_DOMAIN:-${PRIMARY_DOMAIN:-localhost}}
SUPPORTED_DOMAINS: ${SUPPORTED_DOMAINS:-${PRIMARY_DOMAIN:-localhost}}
PROFILE_BASE_DOMAINS: ${PROFILE_BASE_DOMAINS:-${PRIMARY_DOMAIN:-localhost}}
container_name: elektrine_dns
depends_on:
postgres:
condition: service_healthy
env_file:
- ../../.env.production
environment:
DATABASE_URL: ecto://elektrine:${DB_PASSWORD:?set DB_PASSWORD}@postgres/elektrine_prod
POOL_SIZE: ${DNS_POOL_SIZE:-3}
MIX_ENV: prod
RELEASE_NAME: ${RELEASE_NAME:-elektrine}
ELEKTRINE_ENABLED_MODULES: ${ELEKTRINE_ENABLED_MODULES:-all}
ELEKTRINE_RUNTIME_ROLE: worker
ELEKTRINE_ENABLE_WEB: "false"
ELEKTRINE_ENABLE_JOBS: "false"
ELEKTRINE_ENABLE_MAIL: "false"
ELEKTRINE_ENABLE_TOR: "false"
DNS_AUTHORITY_ENABLED: "true"
DNS_RECURSIVE_ENABLED: ${DNS_RECURSIVE_ENABLED:-false}
ports:
- "${DNS_UDP_BIND:-53:5300/udp}"
- "${DNS_TCP_BIND:-53:5300/tcp}"
volumes:
- app_data:/data
networks:
- elektrine_network
restart: unless-stopped
turn:
image: ${TURN_IMAGE:-coturn/coturn:latest@sha256:867607152c6ed9504a0b59a2546a9bd2872dcf74313b3bc6cf9b69cc81be6fb3}
container_name: elektrine_turn
profiles:
- turn
environment:
TURN_HOST: ${TURN_HOST:-${PRIMARY_DOMAIN:-example.com}}
TURN_PORT: ${TURN_PORT:-3478}
TURN_MIN_PORT: ${TURN_MIN_PORT:-49160}
TURN_MAX_PORT: ${TURN_MAX_PORT:-49200}
TURN_REALM: ${TURN_REALM:-${PRIMARY_DOMAIN:-example.com}}
TURN_EXTERNAL_IP: ${TURN_EXTERNAL_IP:-}
TURN_RELAY_IP: ${TURN_RELAY_IP:-}
TURN_SHARED_SECRET: ${TURN_SHARED_SECRET:?set TURN_SHARED_SECRET}
network_mode: host
command:
- --no-cli
- --fingerprint
- --use-auth-secret
- --static-auth-secret=${TURN_SHARED_SECRET:?set TURN_SHARED_SECRET}
- --realm=${TURN_REALM:-${PRIMARY_DOMAIN:-example.com}}
- --server-name=${TURN_REALM:-${PRIMARY_DOMAIN:-example.com}}
- --listening-ip=0.0.0.0
- --listening-port=${TURN_PORT:-3478}
- --min-port=${TURN_MIN_PORT:-49160}
- --max-port=${TURN_MAX_PORT:-49200}
- --lt-cred-mech
- --stale-nonce
- --simple-log
restart: unless-stopped
security_opt:
- no-new-privileges:true
caddy_edge:
build:
context: ../..
dockerfile: deploy/caddy/Dockerfile
container_name: elektrine_caddy_edge
profiles:
- caddy
depends_on:
app:
condition: service_healthy
env_file:
- ../../.env.production
environment:
ACME_EMAIL: ${ACME_EMAIL:-admin@example.com}
CADDY_ASK_URL: ${CADDY_ASK_URL:-http://app_proxy:8080/_edge/tls/v1/allow}
CADDY_ADMIN_HOST: ${CADDY_ADMIN_HOST:-admin.elektrine.com}
CADDY_PROXY_PROTOCOL_ALLOWED_CIDRS: ${CADDY_PROXY_PROTOCOL_ALLOWED_CIDRS:-192.0.2.0/32}
# NETBIRD_ALLOWED_CIDRS kept for Phoenix / docs; Caddy reads literal CIDRs
# from the netbird_allowlist.caddy snippet (synced + caddy reload).
NETBIRD_ALLOWED_CIDRS: ${NETBIRD_ALLOWED_CIDRS:-192.0.2.0/32}
ORIGIN_UPSTREAM: ${CADDY_ORIGIN_UPSTREAM:-app_proxy:8080}
ports:
- "${CADDY_HTTP_BIND:-80:80}"
- "${CADDY_HTTPS_BIND:-443:443}"
volumes:
- ${CADDY_CONFIG_PATH:-../caddy/Caddyfile.baremetal}:/etc/caddy/Caddyfile:ro
- ${CADDY_NETBIRD_ALLOWLIST_PATH:-../caddy/netbird_allowlist.caddy}:/etc/caddy/netbird_allowlist.caddy:ro
- ${CADDY_TLS_MOUNT_DIR:-/opt/elektrine/certs}:/opt/elektrine/certs:ro
- caddy_data:/data
# Proxy network only — no path to Postgres.
networks:
- caddy_proxy_network
restart: unless-stopped
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
# Caddy needs to bind privileged ports when published as 80/443.
cap_add:
- NET_BIND_SERVICE
bluesky_pds:
image: ghcr.io/bluesky-social/pds:0.4@sha256:e0b756701c924410532c61b546a61810b6a0059bcebb0c21718d161f5b7880db
container_name: elektrine_bluesky_pds
profiles:
- bluesky
environment:
PDS_HOSTNAME: ${BLUESKY_MANAGED_DOMAIN:-bsky.local}
PDS_JWT_SECRET: ${BLUESKY_PDS_JWT_SECRET:?set BLUESKY_PDS_JWT_SECRET}
PDS_ADMIN_PASSWORD: ${BLUESKY_MANAGED_ADMIN_PASSWORD:?set BLUESKY_MANAGED_ADMIN_PASSWORD}
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: ${BLUESKY_PDS_ROTATION_KEY_HEX:-}
PDS_DATA_DIRECTORY: /pds
PDS_BLOBSTORE_DISK_LOCATION: /pds/blocks
PDS_BLOB_UPLOAD_LIMIT: 52428800
PDS_DID_PLC_URL: "https://plc.directory"
PDS_BSKY_APP_VIEW_URL: "https://api.bsky.app"
PDS_BSKY_APP_VIEW_DID: "did:web:api.bsky.app"
PDS_REPORT_SERVICE_URL: "https://mod.bsky.app"
PDS_REPORT_SERVICE_DID: "did:plc:ar7c4by46qjdydhdevvrndac"
PDS_CRAWLERS: "https://bsky.network"
LOG_ENABLED: true
ports:
- "127.0.0.1:${BLUESKY_PDS_PORT:-3000}:3000"
volumes:
- bluesky_pds_data:/pds
networks:
# App/backend network for admin/API; proxy network so Caddy can resolve bluesky_pds
- elektrine_network
- caddy_proxy_network
restart: unless-stopped
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
postgres_data:
uploads:
app_data:
caddy_data:
bluesky_pds_data:
networks:
# Backend: app/worker/mail/dns/postgres. Edge proxies must not join this.
elektrine_network:
driver: bridge
# Edge: app + Caddy only. No database on this network.
caddy_proxy_network:
driver: bridge
ipam:
config:
- subnet: ${CADDY_PROXY_DOCKER_SUBNET:-172.30.0.0/24}