# Nextcloud on top of the Media Hub storage network. Overlay on compose.yaml; production # enables it with `COMPOSE_FILE=compose.yaml:compose.nextcloud.yaml` in .env, so plain # `docker compose ...` (and scripts/backup.sh etc.) include it. Test stack: # docker compose -p mediahub-e2e --env-file -f compose.yaml -f compose.nextcloud.yaml up -d --wait # # Nextcloud is unmodified: its primary storage is the S3 gateway (app/s3gw.py), which stores # every object as a Media Hub blob (local copy + encrypted replicas on storage nodes). # Only Caddy publishes a port (HTTPS ${NC_HTTPS_PORT}); s3gw, Nextcloud, its DB and Redis stay # on the compose network. Object storage is chosen at install time: point a FRESH Nextcloud # at it, never switch an existing one. # # Required in the env file: MEDIAHUB_S3_ACCESS_KEY, MEDIAHUB_S3_SECRET_KEY, NEXTCLOUD_DB_PASSWORD, # NEXTCLOUD_ADMIN_PASSWORD, NEXTCLOUD_REDIS_PASSWORD, NC_DOMAIN (LAN IP or name). x-nextcloud-env: &nextcloud-env POSTGRES_HOST: nextcloud-db POSTGRES_DB: nextcloud POSTGRES_USER: nextcloud POSTGRES_PASSWORD: ${NEXTCLOUD_DB_PASSWORD:?set NEXTCLOUD_DB_PASSWORD} NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USER:-admin} NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD:?set NEXTCLOUD_ADMIN_PASSWORD} NEXTCLOUD_TRUSTED_DOMAINS: ${NC_DOMAIN:?set NC_DOMAIN} localhost # Behind Caddy (TLS terminated there): generate https URLs with the public host:port and # trust X-Forwarded-For only from the compose network (brute-force protection sees real IPs). OVERWRITEPROTOCOL: https OVERWRITEHOST: ${NC_DOMAIN}:${NC_HTTPS_PORT:-9443} OVERWRITECLIURL: https://${NC_DOMAIN}:${NC_HTTPS_PORT:-9443} TRUSTED_PROXIES: ${NC_TRUSTED_PROXIES:-172.16.0.0/12} REDIS_HOST: nextcloud-redis REDIS_HOST_PASSWORD: ${NEXTCLOUD_REDIS_PASSWORD:?set NEXTCLOUD_REDIS_PASSWORD} PHP_UPLOAD_LIMIT: 20G PHP_MEMORY_LIMIT: 1G # The image caps request bodies at 1 GiB; single-request WebDAV uploads above that fail # (the official clients chunk, but curl/rclone PUTs don't). APACHE_BODY_LIMIT: "0" OBJECTSTORE_S3_BUCKET: nextcloud OBJECTSTORE_S3_HOST: s3gw OBJECTSTORE_S3_PORT: "9000" OBJECTSTORE_S3_SSL: "false" OBJECTSTORE_S3_USEPATH_STYLE: "true" OBJECTSTORE_S3_REGION: us-east-1 OBJECTSTORE_S3_AUTOCREATE: "true" OBJECTSTORE_S3_KEY: ${MEDIAHUB_S3_ACCESS_KEY:?set MEDIAHUB_S3_ACCESS_KEY} OBJECTSTORE_S3_SECRET: ${MEDIAHUB_S3_SECRET_KEY:?set MEDIAHUB_S3_SECRET_KEY} TZ: ${MEDIAHUB_TIMEZONE:-Asia/Taipei} x-logging: &logging driver: json-file options: max-size: "10m" max-file: "5" services: s3gw: extends: file: compose.yaml service: worker command: ["uvicorn", "app.s3gw:app", "--host", "0.0.0.0", "--port", "9000", "--workers", "2", "--timeout-keep-alive", "30"] environment: MEDIAHUB_S3_ACCESS_KEY: ${MEDIAHUB_S3_ACCESS_KEY} MEDIAHUB_S3_SECRET_KEY: ${MEDIAHUB_S3_SECRET_KEY} cpus: 4 mem_limit: 1g stop_grace_period: 10s healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9000/_health', timeout=4)"] interval: 10s timeout: 5s retries: 12 start_period: 10s nextcloud-db: image: postgres:17-alpine restart: unless-stopped environment: POSTGRES_USER: nextcloud POSTGRES_PASSWORD: ${NEXTCLOUD_DB_PASSWORD} POSTGRES_DB: nextcloud volumes: - nc_pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U nextcloud -d nextcloud"] interval: 5s timeout: 5s retries: 20 logging: *logging # Distributed cache + file locking. Also required for chunked upload v2 (chunks written # straight into an S3 multipart upload instead of being stored twice). nextcloud-redis: image: redis:8-alpine restart: unless-stopped command: ["redis-server", "--requirepass", "${NEXTCLOUD_REDIS_PASSWORD}", "--save", "", "--appendonly", "no"] environment: REDISCLI_AUTH: ${NEXTCLOUD_REDIS_PASSWORD} healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 20 logging: *logging nextcloud: image: nextcloud:34-apache restart: unless-stopped environment: *nextcloud-env volumes: # Code, config.php (instance secrets!), apps. File contents go to the S3 gateway. - nc_html:/var/www/html - ./deploy/nextcloud/hooks/before-starting:/docker-entrypoint-hooks.d/before-starting:ro - ./deploy/nextcloud/hooks/post-upgrade:/docker-entrypoint-hooks.d/post-upgrade:ro - ./deploy/nextcloud/hooks/post-upgrade:/docker-entrypoint-hooks.d/post-installation:ro depends_on: nextcloud-db: condition: service_healthy nextcloud-redis: condition: service_healthy s3gw: condition: service_healthy healthcheck: test: ["CMD-SHELL", "curl -fsS -H 'Host: localhost' http://127.0.0.1/status.php | grep -q '\"installed\":true'"] interval: 15s timeout: 10s retries: 20 start_period: 5m mem_limit: 2g logging: *logging # Background jobs (file scans, previews, trash/version expiry) every 5 minutes. nextcloud-cron: image: nextcloud:34-apache restart: unless-stopped entrypoint: /cron.sh environment: *nextcloud-env volumes: - nc_html:/var/www/html depends_on: nextcloud: condition: service_healthy mem_limit: 1g logging: *logging caddy: environment: NC_SITES: https://${NC_DOMAIN}:${NC_HTTPS_PORT:-9443} ports: - "${HUB_BIND:-127.0.0.1}:${NC_HTTPS_PORT:-9443}:${NC_HTTPS_PORT:-9443}" volumes: - ./deploy/caddy/nextcloud.caddy:/etc/caddy/sites/nextcloud.caddy:ro volumes: nc_pgdata: nc_html: