docker-compose.yml
Rama main · réplica de lectura del repositorio original.
# MDMesh production stack. Configure via .env (generated by ./setup.sh), then:
# Cloudflare Tunnel: docker compose --profile cloudflare up -d
# (by hand, also put COMPOSE_PROFILES=cloudflare in .env, as setup.sh does: --profile alone doesn't reach caddy,
# which then won't believe cloudflared's CF-Connecting-IP; see docker/edge-peers.sh)
# Own domain (HTTPS): docker compose -f docker-compose.yml -f docker-compose.domain.yml up -d
#
# Security defaults: Postgres and the server publish NO host ports; the edge (Caddy) is the only
# thing exposed, and only in the own-domain override. In tunnel mode nothing is published at all —
# cloudflared reaches Caddy over the internal network.
services:
postgres:
image: postgres:14
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-mdmesh}
POSTGRES_USER: ${DB_USER:-mdmesh}
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-mdmesh} -d ${DB_NAME:-mdmesh}"]
interval: 5s
timeout: 5s
retries: 20
server:
# Pull-based updates use the versioned GHCR image; `build:` is the source-deploy fallback (when an
# image tag isn't published locally, compose builds it and tags it with this name). apply.sh bumps
# SERVER_VERSION in .env then `docker compose pull server` to fetch the new signed image.
image: ghcr.io/${IMAGE_OWNER:-local}/mdmesh-server:${SERVER_VERSION:-latest}
build:
context: .
dockerfile: docker/server.Dockerfile
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DB_HOST: postgres
DB_PORT: "5432"
DB_NAME: ${DB_NAME:-mdmesh}
DB_USER: ${DB_USER:-mdmesh}
DB_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
BASE_URL: ${BASE_URL:?set BASE_URL in .env}
HASH_SECRET: ${HASH_SECRET:?set HASH_SECRET in .env}
# Optional; unset = the key the server keeps in its volume. Prefixed .env key for the reason given at JAVA_OPTS.
JWT_SECRET: ${SERVER_JWT_SECRET:-}
SECURE_ENROLLMENT: ${SECURE_ENROLLMENT:-0}
LEGACY_DEVICE_API: ${LEGACY_DEVICE_API:-0}
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-25}
SMTP_FROM: ${SMTP_FROM:-mdm@localhost}
SMTP_USERNAME: ${SMTP_USERNAME:-}
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
SMTP_SSL: ${SMTP_SSL:-false}
SMTP_STARTTLS: ${SMTP_STARTTLS:-false}
# Optional JVM flags, e.g. a temporary DEBUG log config (docs/OPERATIONS.md "Server logs"). Read from the
# prefixed .env key: compose lets the shell override .env, and a generic JAVA_OPTS exported on a Java host must
# not reach the server.
JAVA_OPTS: ${SERVER_JAVA_OPTS:-}
volumes:
- mdmeshdata:/opt/mdmesh
caddy:
# The web image (Caddy + baked SPA). Same pull/build duality as `server`; WEB_VERSION tracks the
# release. apply.sh recreates `server` + `caddy` together so the SPA matches the API.
image: ghcr.io/${IMAGE_OWNER:-local}/mdmesh-web:${WEB_VERSION:-latest}
build:
context: .
dockerfile: docker/web.Dockerfile
# Enrollment-QR parameters baked into the SPA (see docker/web.Dockerfile ARGs). setup.sh
# verifies the latest release APK and persists these in .env; empty values fall back to the
# SPA's debug defaults, matching what release CI bakes into published images.
args:
VITE_AGENT_PACKAGE: ${VITE_AGENT_PACKAGE:-}
VITE_AGENT_CHECKSUM: ${VITE_AGENT_CHECKSUM:-}
VITE_AGENT_APK_URL: ${VITE_AGENT_APK_URL:-}
restart: unless-stopped
depends_on:
- server
- supervisor
environment:
SITE_ADDRESS: ${SITE_ADDRESS:-:80}
ACME_EMAIL: ${ACME_EMAIL:-}
# The hosting mode: in tunnel mode (cloudflare) the edge believes cloudflared's CF-Connecting-IP
# (docker/edge-peers.sh).
COMPOSE_PROFILES: ${COMPOSE_PROFILES:-}
# Proxies in front of the edge (a CDN, a load balancer) whose X-Forwarded-For it believes, as space-separated
# CIDRs; set in .env. Unset or empty: none (docs/OPERATIONS.md, Sign-in limits).
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-}
volumes:
- caddy_data:/data
- caddy_config:/config
# Decoupled updater/recovery service. Polls GitHub for new releases, verifies the signed manifest,
# exposes /update/status, serves /recovery, and (Phase 4) applies updates by driving compose against
# the host daemon. The docker socket is host-root-equivalent — the supervisor is trusted and acts
# only on VERIFIED manifests + AUTHORIZED callers (cookie-forward authz). It NEVER recreates itself
# or postgres; apply.sh touches only `server` + `caddy`.
supervisor:
image: ghcr.io/${IMAGE_OWNER:-local}/mdmesh-supervisor:${SUPERVISOR_VERSION:-latest}
build:
context: .
dockerfile: docker/supervisor.Dockerfile
restart: unless-stopped
environment:
GITHUB_REPO: ${GITHUB_REPO:-}
UPDATE_CHANNEL: ${UPDATE_CHANNEL:-stable}
POLL_INTERVAL_HOURS: ${POLL_INTERVAL_HOURS:-6}
GITHUB_TOKEN: ${GITHUB_TOKEN:-}
CURRENT_VERSION: ${CURRENT_VERSION:-0.0.0}
AUTO_UPDATE: ${AUTO_UPDATE:-0}
# 0 on source installs (IMAGE_OWNER=local): there is no registry image to pull, so the console shows the manual
# upgrade steps instead of a one-click apply that could only back up, fail and roll back. Native sets 0 too.
# Defaults to 0 to match this file's IMAGE_OWNER default (local); setup.sh writes 1 for a registry owner.
APPLY_SUPPORTED: ${APPLY_SUPPORTED:-0}
# Pin project + compose-file set so the supervisor drives the SAME stack the host launched
# (default project name = dir basename would fork a duplicate; COMPOSE_FILE carries the domain
# overlay so recreating caddy keeps its published ports).
COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME:-mdmesh}
COMPOSE_FILE: ${COMPOSE_FILE:-docker-compose.yml}
COMPOSE_PROFILES: ${COMPOSE_PROFILES:-}
DB_USER: ${DB_USER:-mdmesh}
DB_NAME: ${DB_NAME:-mdmesh}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- .:/project
- backups:/backups
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
profiles: ["cloudflare"]
depends_on:
- caddy
command: tunnel --no-autoupdate run
environment:
TUNNEL_TOKEN: ${TUNNEL_TOKEN:-}
volumes:
pgdata:
mdmeshdata:
caddy_data:
caddy_config:
backups: # supervisor pre-update DB dumps + version snapshots + auto.json (survives recreate)