diff --git a/scripts/Common/Software/docker/services/nextcloud/.dockerignore b/scripts/Common/Software/docker/services/nextcloud/.dockerignore new file mode 100644 index 00000000..72e8ffc0 --- /dev/null +++ b/scripts/Common/Software/docker/services/nextcloud/.dockerignore @@ -0,0 +1 @@ +* diff --git a/scripts/Common/Software/docker/services/nextcloud/cache.Dockerfile b/scripts/Common/Software/docker/services/nextcloud/cache.Dockerfile new file mode 100644 index 00000000..fb789522 --- /dev/null +++ b/scripts/Common/Software/docker/services/nextcloud/cache.Dockerfile @@ -0,0 +1,3 @@ +FROM redis +ENV REDIS_HOST_PASSWORD default-password +CMD ["sh", "-c", "exec redis-server --requirepass \"${REDIS_HOST_PASSWORD}\""] diff --git a/scripts/Common/Software/docker/services/nextcloud/cloud.Dockerfile b/scripts/Common/Software/docker/services/nextcloud/cloud.Dockerfile new file mode 100644 index 00000000..20e22b9a --- /dev/null +++ b/scripts/Common/Software/docker/services/nextcloud/cloud.Dockerfile @@ -0,0 +1,33 @@ +FROM nextcloud:fpm + +# Workaround for Nextcloud image not including `bz2` +RUN apt-get update && \ + apt-get install -y \ + libbz2-dev \ + libfcgi-bin && \ + rm -rf /var/lib/apt/lists/* && \ + docker-php-ext-install bz2 + +RUN apt-get update && \ + curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \ + apt-get install -y nodejs && \ + rm -rf /var/lib/apt/lists/* + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libmagickcore-6.q16-6-extra \ + libxss1 \ + libx11-xcb1 \ + wget && \ + wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ + sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' && \ + apt-get update && \ + apt-get install -y google-chrome-stable --no-install-recommends && \ + apt-get remove -y wget gnupg && \ + rm -rf /var/lib/apt/lists/* && \ + rm /etc/apt/sources.list.d/google.list + +RUN curl -Lo /usr/local/bin/php-fpm-healthcheck https://raw.githubusercontent.com/renatomefi/php-fpm-healthcheck/v0.5.0/php-fpm-healthcheck && \ + chmod +x /usr/local/bin/php-fpm-healthcheck + +RUN npm install --global pageres-cli --unsafe-perm diff --git a/scripts/Common/Software/docker/services/nextcloud/docker-compose.base.yml b/scripts/Common/Software/docker/services/nextcloud/docker-compose.base.yml new file mode 100644 index 00000000..4efcb8e7 --- /dev/null +++ b/scripts/Common/Software/docker/services/nextcloud/docker-compose.base.yml @@ -0,0 +1,107 @@ +services: + web: + image: nginx + extends: + file: docker-compose.core.yml + service: web + volumes: + - ./nginx/web.conf:/etc/nginx/nginx.conf + - ./cloud/logs/nginx:/var/log/nginx + depends_on: + core: + condition: service_healthy + installer: + extends: + file: docker-compose.core.yml + service: setup + image: nextcloud:fpm + restart: on-failure + entrypoint: + - bash + - -c + - /entrypoint.sh php-fpm & while ! echo '' 2> /dev/null > /dev/tcp/127.0.0.1/9000; do sleep 1; done; kill -9 $!; true; + db: + image: mariadb:lts + restart: unless-stopped + env_file: db.env + environment: + MARIADB_RANDOM_ROOT_PASSWORD: "yes" + MARIADB_MYSQL_LOCALHOST_USER: 1 + volumes: + - ./cloud/database:/var/lib/mysql + command: + - --innodb_read_only_compressed=OFF + healthcheck: + test: [CMD, healthcheck.sh, --su-mysql, --connect, --innodb_initialized] + start_period: 1m + start_interval: 10s + interval: 1m + timeout: 5s + retries: 3 + cache: + build: + context: . + dockerfile: cache.Dockerfile + restart: unless-stopped + env_file: + - cache.env + volumes: + - ./cloud/cache:/data + healthcheck: + test: [CMD, bash, -c, echo '' > /dev/tcp/127.0.0.1/6379] + interval: 5s + timeout: 3s + retries: 5 + core: + extends: + file: docker-compose.core.yml + service: nextcloud + build: + context: . + dockerfile: cloud.Dockerfile + cap_add: + - SYS_ADMIN + depends_on: &nextcloud-conditions + db: + condition: service_healthy + cache: + condition: service_healthy + installer: + condition: service_completed_successfully + restart: true + healthcheck: + test: [CMD, bash, -c , php-fpm-healthcheck] + start_period: 1m + start_interval: 10s + interval: 1m + timeout: 5s + retries: 5 + cron: + extends: + file: docker-compose.core.yml + service: nextcloud + image: nextcloud:fpm + depends_on: + <<: *nextcloud-conditions + entrypoint: /cron.sh + bridge: + image: shenxn/protonmail-bridge + restart: unless-stopped + volumes: + - ./bridge:/root + turn: + image: instrumentisto/coturn + restart: unless-stopped + collabora: + image: collabora/code + restart: unless-stopped + environment: + dictionaries: de_CH de en + extra_params: '--o:logging.level_startup=warning --o:ssl.enable=true --o:ssl.termination=true --o:user_interface.mode=notebookbar' + volumes: + - /etc/localtime:/etc/localtime:ro + cap_add: + - MKNOD + +volumes: + webroot: {} diff --git a/scripts/Common/Software/docker/services/nextcloud/docker-compose.core.yml b/scripts/Common/Software/docker/services/nextcloud/docker-compose.core.yml new file mode 100644 index 00000000..be8534ed --- /dev/null +++ b/scripts/Common/Software/docker/services/nextcloud/docker-compose.core.yml @@ -0,0 +1,32 @@ +services: + web: + restart: unless-stopped + volumes: + - webroot:/var/www/html:z + - ./php.ini:/usr/local/etc/php/conf.d/nextcloud.ini + - ./cloud/apps:/var/www/html/custom_apps + - ./cloud/config:/var/www/html/config + - ./cloud/data:/var/www/html/data + - ./cloud/themes:/var/www/html/themes + - ./cloud/public:/public + - ../jellyfin/downloads:/downloads + setup: + extends: + service: web + environment: + FCGI_STATUS_PATH: "/fpm-status" + env_file: + - db.env + - cache.env + volumes: + - ./fpm/nextcloud.conf:/usr/local/etc/php-fpm.d/zz-nextcloud.conf + - ./fpm/status.conf:/usr/local/etc/php-fpm.d/zz-status.conf + nextcloud: + extends: + service: setup + env_file: + - nextcloud.env + environment: + MYSQL_HOST: db + REDIS_HOST: cache + TRUSTED_PROXIES: 172.16.0.0/12 diff --git a/scripts/Common/Software/docker/services/nextcloud/docker-compose.secrets.yml b/scripts/Common/Software/docker/services/nextcloud/docker-compose.secrets.yml new file mode 100644 index 00000000..8d18bc0b --- /dev/null +++ b/scripts/Common/Software/docker/services/nextcloud/docker-compose.secrets.yml @@ -0,0 +1,12 @@ +services: + web: + ports: + - 127.0.0.1:1337:80 + turn: + ports: + - 1337:3478/tcp + - 1337:3478/udp + command: [-n, --log-file=stdout, --min-port=49160, --max-port=49200, --use-auth-secret] + collabora: + ports: + - 127.0.0.1:1337:9980 diff --git a/scripts/Common/Software/docker/services/nextcloud/fpm/nextcloud.conf b/scripts/Common/Software/docker/services/nextcloud/fpm/nextcloud.conf new file mode 100644 index 00000000..0fb2e0bb --- /dev/null +++ b/scripts/Common/Software/docker/services/nextcloud/fpm/nextcloud.conf @@ -0,0 +1,5 @@ +[www] +; pm.max_children = 200 +; pm.start_servers = 50 +; pm.min_spare_servers = 50 +; pm.max_spare_servers = 150 diff --git a/scripts/Common/Software/docker/services/nextcloud/fpm/status.conf b/scripts/Common/Software/docker/services/nextcloud/fpm/status.conf new file mode 100644 index 00000000..9badef3b --- /dev/null +++ b/scripts/Common/Software/docker/services/nextcloud/fpm/status.conf @@ -0,0 +1 @@ +pm.status_path = /fpm-status diff --git a/scripts/Common/Software/docker/services/nextcloud/main.fish b/scripts/Common/Software/docker/services/nextcloud/main.fish new file mode 100644 index 00000000..a2a554ec --- /dev/null +++ b/scripts/Common/Software/docker/services/nextcloud/main.fish @@ -0,0 +1,117 @@ +#!/bin/env fish +begin + set -l dir (status dirname) + set -l turn turn + set -l domain cloud "" + set -l service web + set -l office collabora + set -l officeDomain "office" "" + set -l server $service $domain + + set -l services \ + $server \ + $turn turn "" \ + $office $officeDomain + + source "$dir/../service.fish" + + function installSW -V dir -V domain -V service -V turn -V office -V officeDomain + set -l genPW __generatePW + + function $genPW -a length + if [ -z "$length" ] + set length 32 + end + + nix-shell -p keepassxc --run "keepassxc-cli generate --length $length" + end + + set -l file (mktemp) + set -l root (getServiceRoot $argv) + set -l source "$dir/docker-compose.core.yml" + set -l core "$root/$(basename "$source")" + set -l domain (getServiceDomain $domain) + set -l base "$root/docker-compose.base.yml" + set -l baseSource "$dir/$(basename "$base")" + set -l secrets (getServiceSecretsConfig $argv) + set -l secretsSource "$dir/$(basename "$secrets")" + set -l turnKey "$(getServiceKey "$turn")" + set -l portKey "$turnKey.ports[1]" + set -l officeEnv "$(getServiceKey "$office").environment" + set -l dbPW ($genPW) + set -l turnPW ($genPW) + set -l turnPort + set -l redisPW ($genPW) + set -l nextcloudPW ($genPW 64) + + initializeServiceInstallation $argv + sudo cp -r "$dir"/{cache.Dockerfile,cloud.Dockerfile,.dockerignore,fpm,nginx,php.ini} "$root" + + begin + printf "%s\n" \ + "MYSQL_DATABASE=Nextcloud" \ + "MYSQL_USER=nextcloud" \ + "MYSQL_PASSWORD=$dbPW" + end | sudo tee "$root/db.env" >/dev/null + + echo "REDIS_HOST_PASSWORD=$redisPW" | sudo tee "$root/cache.env" >/dev/null + + begin + printf "%s\n" \ + "NEXTCLOUD_ADMIN_USER=admin" \ + "NEXTCLOUD_ADMIN_PASSWORD=$nextcloudPW" + end | sudo tee "$root/nextcloud.env" >/dev/null + + PROTO="https" DOMAIN="$domain" begin + set -l envKey "$(getServiceKey nextcloud).environment" + + yq "$envKey.NEXTCLOUD_TRUSTED_DOMAINS = env.DOMAIN" "$source" | \ + yq "$envKey.OVERWRITEPROTOCOL = env.PROTO" | \ + yq "$envKey.OVERWRITEHOST = env.DOMAIN" | \ + URL="$PROTO://$DOMAIN" yq "$envKey.OVERWRITECLIURL = env.URL" | \ + yq -y . | \ + sudo tee "$core" >/dev/null + + DOMAIN=(getServiceDomain $officeDomain) yq "$officeEnv.server_name = env.DOMAIN" "$baseSource" | \ + URL="https://$(string escape --style regex "$DOMAIN"):443" yq "$officeEnv.aliasgroup1 = env.URL" | \ + yq -y . | \ + sudo tee "$base" >/dev/null + end + + begin + set -l key "$turnKey.command" + + PW="--static-auth-secret=$turnPW" \ + DOMAIN="--realm=$domain" \ + yq "$key |= . + [env.PW, env.DOMAIN]" "$secretsSource" | \ + yq -y . | \ + sudo tee "$secrets" >/dev/null + end + + installDockerService $argv + set turnPort (yq --raw-output (getServicePortKey "$turn") "$secrets" | extractPort) + set turnPort (yq --raw-output "$portKey" "$secrets" | mutatePort "$turnPort") + + cp "$secrets" "$file" + PORT="$turnPort" yq -y "$portKey = env.PORT" "$file" | sudo tee "$secrets" >/dev/null + rm "$file" + end + + function configureSW -V dir + configureDockerService $argv + end + + function getServiceServers -V services + for i in (seq 1 3 (count $services)) + printf "%s\0" $services[(math $i + 1)] $services[(math $i + 2)] + end + end + + function getServiceLocations -V services -a index + set -l i (math (math (math $index - 1) / 2 "*" 3) + 1) + set -l name $services[$i] + printf "%s\0" "$name" / + end + + runInstaller --force $argv +end diff --git a/scripts/Common/Software/docker/services/nextcloud/nginx/web.conf b/scripts/Common/Software/docker/services/nextcloud/nginx/web.conf new file mode 100644 index 00000000..1f5c9842 --- /dev/null +++ b/scripts/Common/Software/docker/services/nextcloud/nginx/web.conf @@ -0,0 +1,185 @@ +worker_processes 8; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + types { + application/javascript cjs mjs; + } + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + sendfile on; + server_tokens off; + keepalive_timeout 65; + + # Set the `immutable` cache control options only for assets with a cache busting `v` argument + map $arg_v $asset_immutable { + "" ""; + default "immutable"; + } + + upstream php-handler { + server core:9000; + } + + server { + listen 80; + + # set max upload size and increase upload timeout: + client_max_body_size 512M; + client_body_timeout 300s; + fastcgi_buffers 64 4K; + + # The settings allow you to optimize the HTTP2 bandwith. + # See https://blog.cloudflare.com/delivering-http-2-upload-speed-improvements/ + # for tuning hints + client_body_buffer_size 512k; + + # Enable gzip but do not remove ETag headers + gzip on; + gzip_vary on; + gzip_comp_level 4; + gzip_min_length 256; + gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; + 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; + + # HTTP response headers + 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; + + # Remove X-Powered-By, which is an information leak + fastcgi_hide_header X-Powered-By; + + # Path to the root of your installation + root /var/www/html; + + # Specify how to handle directories -- specifying `/index.php$request_uri` + # here as the fallback means that Nginx always exhibits the desired behaviour + # when a client requests a path that corresponds to a directory that exists + # on the server. In particular, if that directory contains an index.php file, + # that file is correctly served; if it doesn't, then the request is passed to + # the front-end controller. This consistent behaviour means that we don't need + # to specify custom rules for certain paths (e.g. images and other assets, + # `/updater`, `/ocm-provider`, `/ocs-provider`), and thus + # `try_files $uri $uri/ /index.php$request_uri` + # always provides the desired behaviour. + index index.php index.html /index.php$request_uri; + + # Rule borrowed from `.htaccess` to handle Microsoft DAV clients + location = / { + if ( $http_user_agent ~ ^DavClnt ) { + return 302 /remote.php/webdav/$is_args$args; + } + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + # Make a regex exception for `/.well-known` so that clients can still + # access it despite the existence of the regex rule + # `location ~ /(\.|autotest|...)` which would otherwise handle requests + # for `/.well-known`. + location ^~ /.well-known { + # The rules in this block are an adaptation of the rules + # in `.htaccess` that concern `/.well-known`. + + location = /.well-known/carddav { return 301 /remote.php/dav/; } + location = /.well-known/caldav { return 301 /remote.php/dav/; } + + # Let Nextcloud's API for `/.well-known` URIs handle all other + # requests by passing them to the front-end controller. + return 301 /index.php$request_uri; + } + + # Rules borrowed from `.htaccess` to hide certain paths from clients + location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; } + location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } + + # Ensure this block, which passes PHP files to the PHP process, is above the blocks + # which handle static assets (as seen below). If this block is not declared first, + # then Nginx will encounter an infinite rewriting loop when it prepends `/index.php` + # to the URI, resulting in a HTTP 500 error response. + location ~ \.php(?:$|/) { + # Required for legacy support + rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode\/proxy) /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 on; + + fastcgi_param modHeadersAvailable true; # Avoid sending the security headers twice + fastcgi_param front_controller_active true; # Enable pretty urls + fastcgi_pass php-handler; + + fastcgi_intercept_errors on; + fastcgi_request_buffering off; + + fastcgi_max_temp_file_size 0; + } + + # Javascript mimetype fixes for nginx + # Note: The block below should be removed, and the js|mjs section should be + # added to the block below this one. This is a temporary fix until Nginx + # upstream fixes the js mime-type + location ~* \.(?:js|mjs)$ { + types { + text/javascript js mjs; + } + + try_files $uri /index.php$request_uri; + add_header Cache-Control "public, max-age=15778463, $asset_immutable"; + access_log off; + } + + # Serve static files + location ~ \.(?:css|svg|gif|png|jpg|ico|wasm|tflite|map|ogg|flac)$ { + try_files $uri /index.php$request_uri; + add_header Cache-Control "public, max-age=15778463, $asset_immutable"; + access_log off; # Optional: Don't log access to assets + + location ~ \.wasm$ { + default_type application/wasm; + } + } + + location ~ \.woff2?$ { + try_files $uri /index.php$request_uri; + expires 7d; # Cache-Control policy borrowed from `.htaccess` + access_log off; # Optional: Don't log access to assets + } + + # Rule borrowed from `.htaccess` + location /remote { + return 301 /remote.php$request_uri; + } + + location / { + try_files $uri $uri/ /index.php$request_uri; + } + } +} diff --git a/scripts/Common/Software/docker/services/nextcloud/php.ini b/scripts/Common/Software/docker/services/nextcloud/php.ini new file mode 100644 index 00000000..5f8bd280 --- /dev/null +++ b/scripts/Common/Software/docker/services/nextcloud/php.ini @@ -0,0 +1,5 @@ +memory_limit = 1G +upload_max_filesize = 16G +post_max_size = 1G +max_input_time = 3600 +max_execution_time = 3600 diff --git a/scripts/Common/Software/docker/services/service.fish b/scripts/Common/Software/docker/services/service.fish index 250063a3..8cada5b7 100644 --- a/scripts/Common/Software/docker/services/service.fish +++ b/scripts/Common/Software/docker/services/service.fish @@ -11,7 +11,7 @@ begin end function getPortPattern - echo "^\([.[:digit:]]\+:\)\([[:digit:]]\+\)\(:[[:digit:]]\+\)" + echo "^\([.[:digit:]]\+:\)\?\([[:digit:]]\+\)\(:[[:digit:]]\+\(\/tcp\|udp\)\?\)" end function __substitutePort -a substitution @@ -52,7 +52,7 @@ begin echo "$(getServiceRoot $argv)/$secretsFile" end - function __getServicePortKey -V secretsFile -a name + function getServicePortKey -V secretsFile -a name echo "$(getServiceKey "$name").ports[0]" end @@ -79,7 +79,7 @@ begin argparse -i "comment=" "path=" "url=" -- $argv set -l url set -l config (getServiceSecretsConfig $argv) - set -l portKey (__getServicePortKey "$service") + set -l portKey (getServicePortKey "$service") set -l port (yq --raw-output "$portKey" "$config" | extractPort) if [ -n "$_flag_url" ] @@ -112,6 +112,7 @@ begin end function initializeServiceInstallation -V dir -V nginxRoot + set -l root (getServiceRoot $argv) sudo mkdir -p (getServiceRoot $argv) sudo mkdir -p "$nginxRoot" sudo mkdir -p (dirname (getServiceSecretsConfig $argv)) @@ -129,7 +130,7 @@ begin set -l file (mktemp) set -l port (getRandomPort) set -l service $locations[$j] - set -l portKey (__getServicePortKey "$service") + set -l portKey (getServicePortKey "$service") set -l exposedPort sudo mkdir -p (getServiceRoot $argv) sudo mkdir -p "$nginxRoot"