# Personal local/LAN converter stack. # Do not publish the converter engine port to the host. name: ekko-selfhost services: shared-init: image: busybox:1.37 command: - sh - -c - mkdir -p /data && chown 10001:10001 /data && chmod 700 /data volumes: - profiles_data:/data restart: "no" subconverter: build: context: ./subconverter args: # The engine carries the rules, so it carries their version too: the # gateway asks it at runtime rather than guessing from its own build. EKKO_RULES_VERSION: ${EKKO_RULES_VERSION:-local} EKKO_RULES_BUILT: ${EKKO_RULES_BUILT:-} image: ekko-rules-selfhost-subconverter:local restart: unless-stopped networks: - internal # Intentionally no host ports. Gateway reaches it only on the Docker network. expose: - "25500" read_only: true tmpfs: - /tmp:size=64m,mode=1777 - /base/cache:size=64m,mode=0700,uid=10001,gid=10001 security_opt: - no-new-privileges:true mem_limit: 512m cpus: 1.0 web: build: context: ./app args: EKKO_SITE_VERSION: ${EKKO_SITE_VERSION:-local} SUBCONVERTER_VERSION: ${SUBCONVERTER_VERSION:-v1.3.0} image: ekko-rules-selfhost-web:local restart: unless-stopped depends_on: shared-init: condition: service_completed_successfully subconverter: condition: service_started ports: - "${WEB_BIND_HOST:-0.0.0.0}:${WEB_PORT:-8787}:3000" environment: NODE_ENV: production HOSTNAME: 0.0.0.0 PORT: "3000" WEB_BIND_HOST: ${WEB_BIND_HOST:-0.0.0.0} WEB_PORT: ${WEB_PORT:-8787} LAN_BASE_URL: ${LAN_BASE_URL:-} # lan (default) = personal stored addresses; public = open, stores nothing. SELFHOST_MODE: ${SELFHOST_MODE:-lan} PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-} TRUST_PROXY_HEADERS: ${TRUST_PROXY_HEADERS:-} RATE_LIMIT_MANAGE_PER_MINUTE: ${RATE_LIMIT_MANAGE_PER_MINUTE:-} RATE_LIMIT_SUBSCRIBE_PER_MINUTE: ${RATE_LIMIT_SUBSCRIBE_PER_MINUTE:-} REMOTE_CONFIGS: ${REMOTE_CONFIGS:-} THIRD_PARTY_REMOTE_CONFIGS: ${THIRD_PARTY_REMOTE_CONFIGS:-} ALLOW_CUSTOM_REMOTE_CONFIG: ${ALLOW_CUSTOM_REMOTE_CONFIG:-} SITE_LINKS: ${SITE_LINKS:-} HOST_NETWORK_INFO_PATH: /host-runtime/lan-address.json SUBCONVERTER_BASE_URL: http://subconverter:25500 # EKKO_SITE_VERSION is deliberately absent: the build above bakes it # into the image, so an image always reports the version it was built # from. Setting it here would let a stale value in .env outrank the # image and make a published release report someone else's number. The # rules version is not here either — the engine reports its own. SUBCONVERTER_VERSION: ${SUBCONVERTER_VERSION:-v1.3.0} CONVERT_TIMEOUT_MS: ${CONVERT_TIMEOUT_MS:-30000} MAX_SUBSCRIPTION_BYTES: ${MAX_SUBSCRIPTION_BYTES:-5242880} ACCESS_PASSWORD: ${ACCESS_PASSWORD:-} CONVERT_SHARED_DIR: /shared CONVERT_SHARED_URL_PREFIX: http://web:3000/api/internal/input PROFILE_DATA_DIR: /data CONVERT_ALLOW_HOSTNAMES: ${CONVERT_ALLOW_HOSTNAMES:-} volumes: - profiles_data:/data:rw - ./.runtime:/host-runtime:ro networks: - internal healthcheck: test: - CMD - node - -e - "fetch('http://127.0.0.1:3000/api/health').then((r) => r.json()).then((j) => process.exit(j.subconverter_reachable ? 0 : 1)).catch(() => process.exit(1))" interval: 30s timeout: 5s start_period: 20s retries: 3 read_only: true tmpfs: - /tmp:size=64m,mode=1777 - /shared:size=32m,mode=0700,uid=10001,gid=10001 security_opt: - no-new-privileges:true mem_limit: 512m cpus: 1.0 # Ephemeral fixture server used only by local end-to-end validation. fixture: image: python:3.12-alpine profiles: ["e2e"] working_dir: /srv command: ["python", "/srv/server.py"] volumes: - ./fixtures:/srv:ro networks: - internal volumes: profiles_data: networks: internal: driver: bridge internal: false