# ============================================================ # FreeFrame - optional bundled object storage (Silo, a MinIO fork) # ============================================================ # Usage: # docker compose --env-file .env.prod \ # -f docker-compose.prod.yml -f docker-compose.storage.yml up -d # (add -f docker-compose.images.yml and/or -f docker-compose.https.yml as # you need them; use the same -f flags for every later command) # # FreeFrame has NO local-filesystem storage backend; it speaks only the S3 API. # This file runs an S3 server on this host and creates the bucket. It is the # same Silo setup as the commented-out block in docker-compose.prod.yml, which # is left as it is for installs that already uncommented it. # # In .env.prod set: # S3_STORAGE=minio # S3_ENDPOINT=http://minio:9000 # S3_ACCESS_KEY / S3_SECRET_KEY = strong values # S3_BUCKET = lowercase letters, digits and hyphens only (no underscores) # S3_PUBLIC_ENDPOINT = the address the BROWSER uses to reach storage, because # presigned URLs go to the browser. Either: # * https:// with STORAGE_ROUTED=true (Traefik routes # STORAGE_DOMAIN to Silo over HTTPS; needs docker-compose.https.yml, a # DNS record for STORAGE_DOMAIN, and ports 80 and 443 open), or # * http://:9000 with STORAGE_PORT_BIND=0.0.0.0:9000 for an # HTTP-only install (open port 9000 to the browsers that use FreeFrame). # FRONTEND_URL must be the exact origin people browse to; the app sets the # bucket's CORS rules from it and from CORS_ALLOW_ORIGINS, and Silo enforces # them. # # Optional, all with safe defaults: # STORAGE_ROUTED=false true: Traefik serves STORAGE_DOMAIN over HTTPS. # false keeps Traefik away from Silo, so an # HTTP-mode install never asks Let's Encrypt for # a certificate. # STORAGE_DOMAIN=storage.invalid e.g. storage.review.example.com # STORAGE_PORT_BIND=127.0.0.1:9000 where the S3 port is published on the # host. The console (9001) is never published. # # An existing miniodata volume from the old minio/minio image opens as is. # ============================================================ services: minio: image: pgsty/silo:RELEASE.2026-09-16T00-00-00Z # maintained MinIO fork; minio/minio is no longer pullable (#461) command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${S3_ACCESS_KEY} MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY} volumes: - miniodata:/data ports: - "${STORAGE_PORT_BIND:-127.0.0.1:9000}:9000" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped labels: - traefik.enable=${STORAGE_ROUTED:-false} - traefik.http.routers.minio.rule=Host(`${STORAGE_DOMAIN:-storage.invalid}`) - traefik.http.routers.minio.entrypoints=websecure - traefik.http.routers.minio.tls.certresolver=letsencrypt - traefik.http.services.minio.loadbalancer.server.port=9000 minio-init: # one-shot: create the bucket (idempotent) image: pgsty/mc:RELEASE.2026-09-16T00-00-00Z env_file: - .env.prod # the S3_* vars below are read inside the container depends_on: minio: condition: service_healthy entrypoint: > /bin/sh -c " mc alias set local http://minio:9000 $${S3_ACCESS_KEY} $${S3_SECRET_KEY} && mc mb --ignore-existing local/$${S3_BUCKET} " api: depends_on: minio: condition: service_healthy worker: depends_on: minio: condition: service_healthy email_worker: depends_on: minio: condition: service_healthy maintenance_worker: depends_on: minio: condition: service_healthy beat: depends_on: minio: condition: service_healthy volumes: miniodata: