diff --git a/containers/nextcloud/docker-compose.yml b/containers/nextcloud/docker-compose.yml index f35d8f7..2c9b6d8 100644 --- a/containers/nextcloud/docker-compose.yml +++ b/containers/nextcloud/docker-compose.yml @@ -3,8 +3,13 @@ networks: external: true services: + # PHP-FPM backend. Same container_name as before (nextcloud-service) so the + # auth adapter + tools (`docker exec ... nextcloud-service php occ`) keep + # working unchanged. The image's entrypoint runs occ maintenance:install + # automatically on first boot when the NEXTCLOUD_ADMIN_USER + MYSQL_* env + # vars are set, so no user wizard. nextcloud-service: #LIBREPORTAL|SERVICE_TAG_1|nextcloud-service - image: nextcloud:31-apache + image: nextcloud:31-fpm-alpine container_name: nextcloud-service environment: - TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA @@ -16,10 +21,6 @@ services: - NEXTCLOUD_ADMIN_PASSWORD=NEXTCLOUD_ADMIN_PASSWORD_DATA #LIBREPORTAL|NEXTCLOUD_ADMIN_PASSWORD_TAG|NEXTCLOUD_ADMIN_PASSWORD_DATA - NEXTCLOUD_TRUSTED_DOMAINS=NEXTCLOUD_TRUSTED_DOMAINS_DATA #LIBREPORTAL|NEXTCLOUD_TRUSTED_DOMAINS_TAG|NEXTCLOUD_TRUSTED_DOMAINS_DATA - REDIS_HOST=nextcloud-redis - # GLUETUN_OFF_BEGIN - ports: - - "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1 - # GLUETUN_OFF_END volumes: - ./html:/var/www/html restart: unless-stopped @@ -33,16 +34,6 @@ services: libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA libreportal.backup.db: "mariadb:nextcloud-db:db_data:" libreportal.backup.files: "nextcloud-service:/var/www/html:html:33:33" - traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA - # TRAEFIK_PORT_1_BEGIN - traefik.http.routers.nextcloud-service.entrypoints: web,websecure - traefik.http.routers.nextcloud-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 - traefik.http.routers.nextcloud-service.tls: true - traefik.http.routers.nextcloud-service.tls.certresolver: production - traefik.http.services.nextcloud-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.nextcloud-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 - # TRAEFIK_PORT_1_END - traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA # GLUETUN_OFF_BEGIN networks: DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA @@ -71,12 +62,50 @@ services: nextcloud-redis: #LIBREPORTAL|SERVICE_TAG_3|nextcloud-redis image: redis:alpine container_name: nextcloud-redis + restart: unless-stopped volumes: - ./redis_data:/data networks: DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA ipv4_address: IP_DATA_3 #LIBREPORTAL|IP_TAG_3|IP_DATA_3 + # nginx sidecar — terminates HTTP/static, proxies *.php to nextcloud-service:9000 + # via FastCGI. Shares ./html (read-only) with the FPM container. This is the + # whole performance win over Apache+mod_php: no per-request PHP init, nginx + # handles static + serves connections through the FPM process pool. + nextcloud-web: #LIBREPORTAL|SERVICE_TAG_4|nextcloud-web + image: nginx:alpine + container_name: nextcloud-web + restart: unless-stopped + depends_on: + - nextcloud-service + volumes: + - ./html:/var/www/html:ro + - ./resources/nginx.conf:/etc/nginx/nginx.conf:ro + # GLUETUN_OFF_BEGIN + ports: + - "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1 + # GLUETUN_OFF_END + labels: + traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN + traefik.http.routers.nextcloud-web.entrypoints: web,websecure + traefik.http.routers.nextcloud-web.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 + traefik.http.routers.nextcloud-web.tls: true + traefik.http.routers.nextcloud-web.tls.certresolver: production + traefik.http.services.nextcloud-web.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 + traefik.http.routers.nextcloud-web.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END + traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA + # GLUETUN_OFF_BEGIN + networks: + DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA + ipv4_address: IP_DATA_4 #LIBREPORTAL|IP_TAG_4|IP_DATA_4 + # GLUETUN_OFF_END + # GLUETUN_ON_BEGIN + # network_mode: "container:gluetun-service" + # GLUETUN_ON_END + # >>> libreportal-monitoring >>> #nextcloud-exporter: # container_name: nextcloud-exporter @@ -85,7 +114,7 @@ services: # depends_on: # - nextcloud-service # environment: - # - NEXTCLOUD_SERVER=http://nextcloud-service:PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 + # - NEXTCLOUD_SERVER=http://nextcloud-web:PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 # - NEXTCLOUD_USERNAME=NEXTCLOUD_ADMIN_USER_DATA #LIBREPORTAL|NEXTCLOUD_ADMIN_USER_TAG|NEXTCLOUD_ADMIN_USER_DATA # - NEXTCLOUD_PASSWORD=NEXTCLOUD_ADMIN_PASSWORD_DATA #LIBREPORTAL|NEXTCLOUD_ADMIN_PASSWORD_TAG|NEXTCLOUD_ADMIN_PASSWORD_DATA # - NEXTCLOUD_TLS_SKIP_VERIFY=true @@ -93,4 +122,3 @@ services: # networks: # DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA # <<< libreportal-monitoring <<< - diff --git a/containers/nextcloud/resources/nginx.conf b/containers/nextcloud/resources/nginx.conf new file mode 100644 index 0000000..1aa5eff --- /dev/null +++ b/containers/nextcloud/resources/nginx.conf @@ -0,0 +1,90 @@ +# Nginx sidecar config for the Nextcloud PHP-FPM container. Based on the +# Nextcloud admin-manual's recommended config, trimmed to what we need behind +# Traefik (no TLS here — Traefik terminates). + +worker_processes auto; +events { worker_connections 1024; } + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + server_tokens off; + + keepalive_timeout 65; + client_max_body_size 10G; # large uploads + fastcgi_buffers 64 4K; + + gzip on; + gzip_vary on; + gzip_comp_level 4; + gzip_min_length 256; + gzip_proxied any; + gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; + + # Traefik does the TLS, so Nextcloud sees its forwarded headers. + map $http_x_forwarded_proto $forwarded_scheme { + default off; + https on; + } + + upstream php-handler { + server nextcloud-service:9000; + } + + server { + listen 80; + server_name _; + root /var/www/html; + + # Security headers (Nextcloud expects these; Traefik may also add some) + add_header Referrer-Policy "no-referrer" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Permitted-Cross-Domain-Policies "none" always; + add_header X-Robots-Tag "noindex, nofollow" always; + add_header X-XSS-Protection "1; mode=block" always; + fastcgi_hide_header X-Powered-By; + + # .well-known redirects (CalDAV / CardDAV / WebFinger / NodeInfo) + location = /.well-known/carddav { return 301 /remote.php/dav/; } + location = /.well-known/caldav { return 301 /remote.php/dav/; } + location = /.well-known/webfinger{ return 301 /index.php$uri; } + location = /.well-known/nodeinfo { return 301 /index.php$uri; } + + # Block sensitive paths + location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; } + location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } + + index index.php index.html /index.php$request_uri; + try_files $uri $uri/ /index.php$request_uri; + + location ~ \.php(?:$|/) { + rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+|.+\/richdocumentscode\/proxy)/.+\.php$ /index.php$request_uri; + + fastcgi_split_path_info ^(.+?\.php)(\/.*|)$; + set $path_info $fastcgi_path_info; + try_files $fastcgi_script_name =404; + + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $path_info; + fastcgi_param HTTPS $forwarded_scheme; + fastcgi_param modHeadersAvailable true; + fastcgi_param front_controller_active true; + fastcgi_pass php-handler; + fastcgi_intercept_errors on; + fastcgi_request_buffering off; + fastcgi_read_timeout 300; + } + + location ~ \.(?:css|js|svg|gif|png|jpg|ico|woff2?|otf|wasm|tflite|map|html|json)$ { + try_files $uri /index.php$request_uri; + expires 6M; + access_log off; + } + } +} diff --git a/containers/nextcloud/scripts/nextcloud_update_specifics.sh b/containers/nextcloud/scripts/nextcloud_update_specifics.sh new file mode 100644 index 0000000..2501bb8 --- /dev/null +++ b/containers/nextcloud/scripts/nextcloud_update_specifics.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Post-install/update specifics for Nextcloud — dispatched by appUpdateSpecifics. +# +# Nextcloud's official image runs `occ maintenance:install` on first boot from the +# NEXTCLOUD_ADMIN_USER + MYSQL_* env vars (so no web wizard). After that, wire +# Redis as the distributed cache + file-locking backend — this is the actual +# performance win for sync-heavy workloads. The redis container is on the same +# docker network as nextcloud-service, so the hostname `nextcloud-redis` resolves. +# Idempotent: `occ config:system:set` with the same value is a no-op. +appUpdateSpecifics_nextcloud() { + # Wait for Nextcloud's first-boot init to complete (config.php exists + + # occ reports installed). 60 × 2s = 2 minute timeout. + local i=0 + while (( i++ < 60 )); do + if runFileOp docker exec -u www-data nextcloud-service test -f /var/www/html/config/config.php 2>/dev/null \ + && runFileOp docker exec -u www-data nextcloud-service php occ status 2>/dev/null | grep -q 'installed: true'; then + break + fi + sleep 2 + done + if (( i >= 60 )); then + isNotice "Nextcloud not yet initialized; skipping Redis wiring (re-run by reinstalling once it's up)" + return 0 + fi + + isNotice "Wiring Redis caching into Nextcloud..." + local rc=0 + runFileOp docker exec -u www-data nextcloud-service php occ config:system:set memcache.distributed --value '\OC\Memcache\Redis' >/dev/null 2>&1 || rc=1 + runFileOp docker exec -u www-data nextcloud-service php occ config:system:set memcache.locking --value '\OC\Memcache\Redis' >/dev/null 2>&1 || rc=1 + runFileOp docker exec -u www-data nextcloud-service php occ config:system:set redis host --value nextcloud-redis >/dev/null 2>&1 || rc=1 + runFileOp docker exec -u www-data nextcloud-service php occ config:system:set redis port --value 6379 --type=integer >/dev/null 2>&1 || rc=1 + runFileOp docker exec -u www-data nextcloud-service php occ config:system:set filelocking.enabled --value true --type=boolean >/dev/null 2>&1 || rc=1 + if [[ $rc -eq 0 ]]; then + isSuccessful "Nextcloud → Redis caching + file-locking wired." + else + isNotice "Redis wiring had errors (non-fatal; Nextcloud still functions without it)" + fi +}