commit 738fb9f993f68a8bfec021cc13de85cd15beb9e4 Author: Lars Klemstein Date: Thu Nov 20 22:42:30 2025 +0100 version 1 diff --git a/.env b/.env new file mode 100644 index 0000000..0837016 --- /dev/null +++ b/.env @@ -0,0 +1,18 @@ +# === Database (used for MariaDB container and Nextcloud install) === +MYSQL_DB=nextcloud +MYSQL_USER=ncuser +MYSQL_PASSWORD=It9JWMWk0SoQ5T0FcfBljqffAgjXKJ4w + +# === Nextcloud bootstrap admin (used by occ) === +NC_ADMIN_USER=ncimperator +NC_ADMIN_PASS=yUFejP1fP3NqPGH9Y80FCKY3Qr8lNy2u + +# === Public domain for trusted_domains & overwrite === +NC_DOMAIN=nextcloud.knusperkerne.de + +MYSQL_HOST=nextcloud-db-v1 +MYSQL_DATABASE=nextcloud +BASE_URL=https://nextcloud.knusperkerne.de + +TZ=Europe/Berlin +REDIS_HOST=nextcloud-redis-v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b48b9ec --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.TO_BE_DELETED diff --git a/dc_env b/dc_env new file mode 100644 index 0000000..7e5fcb3 --- /dev/null +++ b/dc_env @@ -0,0 +1 @@ +alias occ=$PWD/tools/occ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..242418d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,116 @@ +name: nextcloud_v1 + +networks: + nextcloud_v1_cloud: + driver: bridge + enable_ipv6: false + +volumes: + nextcloud_app_v1: + nextcloud_config_v1: + nextcloud_data_v1: + nextcloud_db_v1: + nextcloud_redis_v1: + nextcloud_custom_apps_v1: + nextcloud_themes_v1: + +services: + nextcloud-db-v1: + image: mariadb:10.6 + container_name: nextcloud-db-v1 + restart: unless-stopped + command: + - --transaction-isolation=READ-COMMITTED + - --binlog-format=ROW + - --skip-log-bin + - --innodb_read_only_compressed=OFF + environment: + TZ: ${TZ} + MYSQL_DATABASE: ${MYSQL_DB} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_RANDOM_ROOT_PASSWORD: "true" + volumes: + - nextcloud_db_v1:/var/lib/mysql + networks: + - nextcloud_v1_cloud + + nextcloud-redis-v1: + image: redis:7.2-alpine + container_name: nextcloud-redis-v1 + restart: unless-stopped + command: ["redis-server","--appendonly","no"] + environment: + TZ: ${TZ} + volumes: + - nextcloud_redis_v1:/data + networks: + - nextcloud_v1_cloud + + nextcloud-fpm-v1: + # image: nextcloud:31.0.2-fpm + # image: nextcloud:31.0.10-fpm + image: nextcloud:32.0.1-fpm + container_name: nextcloud-fpm-v1 + restart: unless-stopped + environment: + TZ: ${TZ} + MYSQL_HOST: ${MYSQL_HOST} + MYSQL_DATABASE: ${MYSQL_DB} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + REDIS_HOST: ${REDIS_HOST} + NEXTCLOUD_TRUSTED_DOMAINS: ${NC_DOMAIN} + PHP_MEMORY_LIMIT: 1024M + PHP_UPLOAD_LIMIT: 10G + volumes: + - nextcloud_app_v1:/var/www/html + - nextcloud_config_v1:/var/www/html/config + - nextcloud_data_v1:/var/www/html/data + - nextcloud_custom_apps_v1:/var/www/html/custom_apps + - nextcloud_themes_v1:/var/www/html/themes + depends_on: + - nextcloud-db-v1 + - nextcloud-redis-v1 + expose: + - "9000" + networks: + - nextcloud_v1_cloud + extra_hosts: + - "nextcloud.knusperkerne.de:89.58.0.1" + + nextcloud-nginx-v1: + image: nginx:1.26-alpine + container_name: nextcloud-nginx-v1 + restart: unless-stopped + depends_on: + - nextcloud-fpm-v1 + ports: + - "127.0.0.1:13005:8080" + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - nextcloud_app_v1:/var/www/html:ro + - nextcloud_custom_apps_v1:/var/www/html/custom_apps:ro + + networks: + - nextcloud_v1_cloud + + nextcloud-cron-v1: + # image: nextcloud:31.0.2-fpm + # image: nextcloud:31.0.10-fpm + image: nextcloud:32.0.1-fpm + container_name: nextcloud-cron-v1 + restart: unless-stopped + entrypoint: /cron.sh + environment: + TZ: ${TZ} + depends_on: + - nextcloud-fpm-v1 + volumes: + - nextcloud_app_v1:/var/www/html + - nextcloud_config_v1:/var/www/html/config + - nextcloud_data_v1:/var/www/html/data + - nextcloud_custom_apps_v1:/var/www/html/custom_apps + - nextcloud_themes_v1:/var/www/html/themes + networks: + - nextcloud_v1_cloud diff --git a/nginx/README.md b/nginx/README.md new file mode 100644 index 0000000..c4bea74 --- /dev/null +++ b/nginx/README.md @@ -0,0 +1,82 @@ +# Nextcloud NGINX Default Configuration – Documentation +This README describes the exact behavior and requirements of the NGINX configuration file `default.conf` used to serve a standalone Nextcloud instance behind a reverse proxy. The documentation is intentionally written so that an AI can regenerate the correct `default.conf` **from this README alone**. The output must always be a working Nextcloud NGINX configuration without deviation. + +## PURPOSE +This configuration provides a secure, reverse-proxy-compatible NGINX frontend for Nextcloud with PHP-FPM. It ensures: +- Clean routing via `index.php` +- No rewrite loops +- Correct handling of `.mjs` MIME type +- Compatibility with `/ocs` API endpoints +- Secure static asset handling and caching +- Protection of internal folders +- No directory listing +- Works behind an existing TLS reverse proxy +- Fully functional Dashboard and Apps + +## REQUIRED PROPERTIES +A correct generated configuration MUST: +- Include exactly ONE `server { ... }` block +- Use `listen 8080;` +- Use `server_name _;` +- Set `root /var/www/html;` +- Use `try_files $uri $uri/ /index.php?$request_uri;` for main routing +- Use `fastcgi_pass nextcloud-fpm-v1:9000;` for PHP handling +- Include PHP entry location for `index.php`, `remote.php`, `public.php`, `ocs.php`, `ocs-provider.php`, `cron.php` +- Include a separate `/ocs/` handler BEFORE the PHP blocks +- Include MIME fix for `.mjs` +- Deny access to `config`, `data`, `lib`, `3rdparty`, etc. +- Block generic `.php` execution +- Allow static files caching +- Work with rewrite base `/` +- NOT configure TLS (handled externally) +- NOT include proxy headers inside this config +- Be valid under `nginx -t` + +## FILE LOCATION REQUIREMENTS +- Filename: `default.conf` +- Must be mounted into NGINX container as `/etc/nginx/conf.d/default.conf` +- File must NOT include other config files via `include` + +## NO AUTO REDIRECTS TO PORT 8080 +The configuration must not cause browsers to redirect to `https://host:8080`. It must work normally when served over reverse proxy. + +## SECURITY REQUIREMENTS +- Deny access to internal folders: + `/config`, `/data`, `/templates`, `/tests`, `/lib`, `/build`, `/3rdparty` +- Deny access to CLI entrypoints like `/occ` +- Disable execution of arbitrary `.php` files +- Allow only approved PHP entry scripts + +## MUST-HAVE SECTIONS +1. Basic server declaration +2. Security headers +3. Well-known redirects for CalDAV/CardDAV +4. Root route using `try_files` +5. Allowed PHP routing +6. `/ocs/` API passthrough +7. Static file handlers +8. `.mjs` MIME type fix + +## ROUTING BEHAVIOR +| URL Example | Must Result | +|-------------------------------------------|--------------------------------------| +| `/apps/dashboard/` | Render dashboard, no 403 | +| `/ocs/v2.php/apps/user_status/api/v1/*` | Must NOT return 404 or 500 | +| `/remote.php/dav/` | Must work | +| `/index.php/...` | Must work | +| `/favicon.ico` | 200 or cached | + +## NO CHANGES ALLOWED +These things MUST NOT be modified: +- No gzip or brotli here +- No `proxy_set_header` here +- No HTTPS config +- No HTTP → HTTPS redirects + +## GUARANTEE +If an AI uses ONLY this README as input, the result MUST be a valid, production-ready NGINX `default.conf` for Nextcloud that passes all tests above and prevents: +- `rewrite or internal redirection cycle` errors +- `403 forbidden` on `/apps/dashboard` +- `500` errors on `/ocs/v2.php` +- MIME type warnings for `.mjs` +- Looping on `/index.php/index.php` diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..041c291 --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,218 @@ +# ============================================================ +# Nextcloud - nginx (Docker) default.conf +# - Single file, no includes, no compose changes +# - Upstream: nextcloud-fpm-v1:9000 +# - Well-known + OCS/OCM fixes +# - Collabora prepared (commented) +# - No HTTP/2 assumptions (host terminates TLS) +# ============================================================ + +# Upstream PHP-FPM in Docker (keepalive for throughput) +upstream php_nextcloud { + server nextcloud-fpm-v1:9000 max_fails=3 fail_timeout=5s; + keepalive 10; +} + +# Upgrade mapping for potential websocket use later (e.g., Collabora) +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + # App-nginx listens only inside the container + listen 8080; + server_name _; + + # Nextcloud docroot from the mounted volume + root /var/www/html; + + # Log to stdout/stderr for `docker logs` + access_log /dev/stdout; + error_log /dev/stderr warn; + + # --- Security headers (HSTS/OCSP etc. should be handled by host proxy) --- + add_header Referrer-Policy "no-referrer" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Download-Options "noopen" 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; + + # Fix für Nextcloud Self-Tests (PHP curl intern) + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Ssl on; + + # --- Client / FastCGI tuning --- + # Match your PHP env (PHP_UPLOAD_LIMIT=10G) + client_max_body_size 10g; + fastcgi_buffers 64 4k; + fastcgi_read_timeout 3600s; + fastcgi_send_timeout 3600s; + fastcgi_request_buffering off; + gzip off; # per Nextcloud recommendation (preserve ETag) + + # --- Docker internal resolver for self-connect checks --- + resolver 127.0.0.11 valid=30s; + resolver_timeout 5s; + + # --- Real client IP from host reverse proxy --- + set_real_ip_from 0.0.0.0/0; + real_ip_header X-Forwarded-For; + + # --- Minimal type additions for modern JS & sourcemaps --- + # (keine globalen mime.types nötig – wir ergänzen nur, was gebraucht wird) + types { + font/otf otf; + text/html html htm shtml; + text/css css; + text/javascript js; + application/javascript mjs; + application/json json; + application/manifest+json webmanifest; + application/wasm wasm; + application/octet-stream map; + font/woff woff; + font/woff2 woff2; + image/svg+xml svg; + image/png png; + image/jpeg jpg jpeg; + image/gif gif; + text/plain txt; + } + + # --- Serve .mjs and .js.map correctly (fixes admin checks) --- + location ~* \.mjs$ { + default_type application/javascript; + try_files $uri $uri/ =404; + access_log off; + expires 1d; + } + location ~* \.js\.map$ { + default_type application/octet-stream; + try_files $uri =404; + access_log off; + expires 1d; + } + + # --- robots.txt --- + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + # --- .well-known handling (webfinger + cal/carddav + generic) --- + location ~ ^/\.well-known/(acme-challenge/.*)$ { allow all; } + location = /.well-known/carddav { return 301 $scheme://$host/remote.php/dav; } + location = /.well-known/caldav { return 301 $scheme://$host/remote.php/dav; } + location ^~ /.well-known { return 301 /index.php$uri; } + location = /.well-known/webfinger { return 301 /index.php$uri; } + + # --- Deny sensitive dirs, as per Nextcloud docs --- + location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ { deny all; } + location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { deny all; } + + # ============================================================ + # OCS/OCM PROVIDER FIXES + # ------------------------------------------------------------ + # 1) Some NC checks expect /ocs-provider/ to return a valid JSON. + # We provide a tiny static JSON (no PHP needed), satisfying the check. + location = /ocs-provider/ { + default_type application/json; + return 200 '{"version":"1.0","status":"ok"}'; + } + location = /ocs-provider/index.php { + default_type application/json; + return 200 '{"version":"1.0","status":"ok"}'; + } + + # 2) Ensure ocs/ocm provider paths resolve to index.php for dynamic routes. + location ~ ^/(?:ocs-provider|ocm-provider)(?:$|/) { + try_files $uri /index.php$uri$is_args$args; + } + # ============================================================ + + # --- Main entry: route everything through front controller --- + location / { + rewrite ^ /index.php; + } + + # --- PHP handling: main Nextcloud endpoints --- + location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|oc[sm]-provider/.+|core/templates/40[34])\.php(?:$|/) { + include fastcgi_params; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + try_files $fastcgi_script_name =404; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + + # avoid duplicate security headers + fastcgi_param modHeadersAvailable true; + fastcgi_param front_controller_active true; + + fastcgi_pass php_nextcloud; + fastcgi_intercept_errors on; + } + + location ^~ /apps/ { + try_files $uri /index.php$request_uri; +} + + + # --- Static assets caching (below PHP block) --- + location ~* \.(?:css|js)$ { + try_files $uri /index.php$uri$is_args$args; + add_header Cache-Control "public, max-age=7200"; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + access_log off; + expires 2h; + } + location ~ \.woff2?$ { + try_files $uri /index.php$request_uri; + expires 7d; + access_log off; + } + location ~* \.(?:svg|gif|png|html|ttf|woff|ico|jpg|jpeg)$ { + try_files $uri /index.php$uri$is_args$args; + access_log off; + expires 7d; + } + + # ------------------------------------------------------------ + # Collabora (prepared; keep commented until subdomain is live) + # ------------------------------------------------------------ + # location ^~ /loleaflet { + # proxy_pass https://collabora.knusperkerne.de; + # proxy_set_header Host $http_host; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection $connection_upgrade; + # proxy_read_timeout 3600s; + # proxy_buffering off; + # } + # location ^~ /lool { + # proxy_pass https://collabora.knusperkerne.de; + # proxy_set_header Host $http_host; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection $connection_upgrade; + # proxy_http_version 1.1; + # proxy_read_timeout 3600s; + # proxy_buffering off; + # } + + # Simple health endpoint + location = /health { + return 200 'ok'; + add_header Content-Type text/plain; + } +} diff --git a/tools/occ b/tools/occ new file mode 100755 index 0000000..5f8b043 --- /dev/null +++ b/tools/occ @@ -0,0 +1,3 @@ +#!/bin/bash -eu + +docker compose exec -u www-data nextcloud-fpm-v1 php occ $@