diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..176a458f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 6f56059d..aca2e718 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -1,15 +1,16 @@ --- -name: 🐛 Bug report -about: Help us improving by reporting a bug -labels: bug, 0. Needs triage +name: 🐛 Bug report - no questions and no support! +about: Help us improving by reporting a bug - this category is not for questions and also not for support! Please use one of the options below for questions and support +labels: 0. Needs triage --- - -### How to use GitHub - -* Please use the 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to show that you are affected by the same issue. -* Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue. -* Subscribe to receive notifications on status change and new comments. + ### Steps to reproduce @@ -22,11 +23,17 @@ labels: bug, 0. Needs triage ### Actual behavior -### Host OS +### Other information +#### Host OS +#### Output of `sudo docker info` -#### Nextcloud AIO version +#### Docker run command or docker-compose file that you used -#### Current channel +#### Output of `sudo docker logs nextcloud-aio-mastercontainer` -#### Other valuable info +#### Output of `sudo docker inspect nextcloud-aio-mastercontainer` + +#### Output of `sudo docker ps -a` + +#### Other valuable info diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md index c7d8f285..2cc54fb4 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -1,15 +1,9 @@ --- name: 📖 Existing feature/documentation enhancement about: Suggest an enhancement of an existing feature/documentation - for other types, please use the feature request option below -labels: enhancement, 0. Needs triage +labels: 0. Needs triage --- - -### How to use GitHub -* Please use the 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to show that you are interested into the same feature. -* Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue. -* Subscribe to receive notifications on status change and new comments. - ### Is your feature request related to a problem? Please describe. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index bb669bb6..72ae238a 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,14 +1,14 @@ blank_issues_enabled: false contact_links: + - name: 📘 Documentation on Nextcloud AIO + url: https://github.com/nextcloud/all-in-one#faq + about: Please read the docs first before submitting any report or request! + - name: ⛑️ Questions and support + url: https://help.nextcloud.com/tag/aio + about: For questions, support and help - name: 💡 Suggest a new feature or discuss one url: https://github.com/nextcloud/all-in-one/discussions/categories/ideas about: For new feature requests and discussion of existing ones - - name: ❓ Questions on AIO - url: https://github.com/nextcloud/all-in-one/discussions/categories/questions - about: For questions regarding AIO - - name: ⛑️ Community Support and Help - url: https://help.nextcloud.com/tag/aio - about: For other types of questions - name: 💼 Nextcloud Enterprise url: https://portal.nextcloud.com/ - about: If you are a Nextcloud Enterprise customer, or need Professional support, so it can be resolved directly by our dedicated engineers more quickly \ No newline at end of file + about: If you are a Nextcloud Enterprise customer, or need Professional support, so it can be resolved directly by our dedicated engineers more quickly diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 71bf1f03..7fe1067e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,158 +1,62 @@ version: 2 updates: - package-ecosystem: "github-actions" - directory: "/" + directory: ".github/workflows" schedule: interval: "daily" time: "12:00" open-pull-requests-limit: 10 + rebase-strategy: "disabled" + labels: + - 3. to review + - dependencies + cooldown: + default-days: 7 - package-ecosystem: composer directory: "/php/" schedule: interval: "daily" time: "12:00" open-pull-requests-limit: 10 + rebase-strategy: "auto" labels: - 3. to review - dependencies - package-ecosystem: "docker" - directory: "/Containers/apache" + directories: + - "/Containers/alpine" + - "/Containers/apache" + - "/Containers/borgbackup" + - "/Containers/clamav" + - "/Containers/collabora" + - "/Containers/docker-socket-proxy" + - "/Containers/domaincheck" + - "/Containers/fulltextsearch" + - "/Containers/imaginary" + - "/Containers/mastercontainer" + - "/Containers/nextcloud" + - "/Containers/notify-push" + - "/Containers/onlyoffice" + - "/Containers/postgresql" + - "/Containers/redis" + - "/Containers/talk" + - "/Containers/talk-recording" + - "/Containers/watchtower" + - "/Containers/whiteboard" schedule: interval: "daily" - time: "12:00" + time: "04:00" open-pull-requests-limit: 10 + rebase-strategy: "disabled" labels: - 3. to review - dependencies -- package-ecosystem: "docker" - directory: "/Containers/borgbackup" - schedule: - interval: "daily" - time: "12:00" - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/collabora" - schedule: - interval: "daily" - time: "12:00" - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/domaincheck" - schedule: - interval: "daily" - time: "12:00" - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/mastercontainer" - schedule: - interval: "daily" - time: "12:00" ignore: - dependency-name: "php" update-types: ["version-update:semver-major", "version-update:semver-minor"] - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/nextcloud" - schedule: - interval: "daily" - time: "12:00" - ignore: - - dependency-name: "php" - update-types: ["version-update:semver-major", "version-update:semver-minor"] - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/postgresql" - schedule: - interval: "daily" - time: "12:00" - ignore: - dependency-name: "postgres" update-types: ["version-update:semver-major"] - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/redis" - schedule: - interval: "daily" - time: "12:00" - ignore: - dependency-name: "redis" update-types: ["version-update:semver-major"] - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/talk" - schedule: - interval: "daily" - time: "12:00" - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/watchtower" - schedule: - interval: "daily" - time: "12:00" - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/clamav" - schedule: - interval: "daily" - time: "12:00" - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/onlyoffice" - schedule: - interval: "daily" - time: "12:00" - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/imaginary" - schedule: - interval: "daily" - time: "12:00" - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies -- package-ecosystem: "docker" - directory: "/Containers/fulltextsearch" - schedule: - interval: "daily" - time: "12:00" - ignore: - dependency-name: "elasticsearch" update-types: ["version-update:semver-major"] - open-pull-requests-limit: 10 - labels: - - 3. to review - - dependencies diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..0350cecc --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,8 @@ + + +* Resolves: # +* [Sign-off message](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md) is added to all commits diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..e636d746 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,14 @@ +changelog: + categories: + - title: 🏕 New features and other improvements + labels: + - enhancement + - title: 🐞 Fixed bugs + labels: + - bug + - title: 👒 Updated dependencies + labels: + - dependencies + - title: 📄 Improved documentation + labels: + - documentation diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 00000000..475940a9 --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,20 @@ +name: 'Codespell' + +on: + pull_request: + push: + branches: + - main + +jobs: + codespell: + name: Check spelling + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Check spelling + uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2 + with: + check_filenames: true + check_hidden: true diff --git a/.github/workflows/collabora.yml b/.github/workflows/collabora.yml new file mode 100644 index 00000000..a61067f3 --- /dev/null +++ b/.github/workflows/collabora.yml @@ -0,0 +1,29 @@ +name: collabora-update + +on: + workflow_dispatch: + schedule: + - cron: '00 12 * * *' + +jobs: + collabora-update: + name: update collabora + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run collabora-profile-update + run: | + rm -f php/cool-seccomp-profile.json + wget https://raw.githubusercontent.com/CollaboraOnline/online/refs/heads/main/docker/cool-seccomp-profile.json + mv cool-seccomp-profile.json php/ + + - name: Create Pull Request + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7 + with: + commit-message: collabora-seccomp-update automated change + signoff: true + title: collabora seccomp update + body: Automated collabora seccomp profile update + labels: dependencies, 3. to review + milestone: next + branch: collabora-seccomp-update diff --git a/.github/workflows/command-rebase.yml b/.github/workflows/command-rebase.yml deleted file mode 100644 index 2279065b..00000000 --- a/.github/workflows/command-rebase.yml +++ /dev/null @@ -1,51 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization - -name: Rebase command - -on: - issue_comment: - types: created - -permissions: - contents: read - -jobs: - rebase: - runs-on: ubuntu-latest - permissions: - contents: none - - # On pull requests and if the comment starts with `/rebase` - if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/rebase') - - steps: - - name: Add reaction on start - uses: peter-evans/create-or-update-comment@v2 - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - repository: ${{ github.event.repository.full_name }} - comment-id: ${{ github.event.comment.id }} - reaction-type: "+1" - - - name: Checkout the latest code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - token: ${{ secrets.COMMAND_BOT_PAT }} - - - name: Automatic Rebase - uses: cirrus-actions/rebase@1.7 - env: - GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }} - - - name: Add reaction on failure - uses: peter-evans/create-or-update-comment@v2 - if: failure() - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - repository: ${{ github.event.repository.full_name }} - comment-id: ${{ github.event.comment.id }} - reaction-type: "-1" diff --git a/.github/workflows/community-containers.yml b/.github/workflows/community-containers.yml new file mode 100644 index 00000000..5271bfa8 --- /dev/null +++ b/.github/workflows/community-containers.yml @@ -0,0 +1,37 @@ +name: Validate community containers + +on: + pull_request: + paths: + - 'community-containers/**' + push: + branches: + - main + paths: + - 'community-containers/**' + +jobs: + validator-community-containers: + name: Validate community containers + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Validate structure + run: | + CONTAINERS="$(find ./community-containers -mindepth 1 -maxdepth 1 -type d)" + mapfile -t CONTAINERS <<< "$CONTAINERS" + for container in "${CONTAINERS[@]}"; do + container="$(echo "$container" | sed 's|./community-containers/||')" + if ! [ -f ./community-containers/"$container"/"$container.json" ]; then + echo ".json file must be named like its parent folder $container" + FAIL=1 + fi + if ! [ -f ./community-containers/"$container"/readme.md ]; then + echo "There must be a readme.md file in the folder!" + FAIL=1 + fi + if [ -n "$FAIL" ]; then + exit 1 + fi + done diff --git a/.github/workflows/create-psalm-container.yml b/.github/workflows/create-psalm-container.yml deleted file mode 100644 index d4b021a3..00000000 --- a/.github/workflows/create-psalm-container.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Create Psalm Container - -on: - workflow_dispatch: - schedule: - - cron: '5 4 * * *' - -jobs: - push_to_registry: - runs-on: ubuntu-latest - - name: Create Psalm Container - - permissions: - packages: write - contents: read - - steps: - - name: Check out the repo - run: | - git clone https://github.com/psalm/psalm-github-actions.git - - - name: Modify the Dockerfile - run: | - set -x - sed -i 's|FROM php:7.4-alpine|FROM php:8.0-alpine|' "psalm-github-actions/Dockerfile" - cat << APCU >> "psalm-github-actions/Dockerfile" - RUN mkdir -p /usr/src/php/ext/apcu && \ - curl -fsSL https://pecl.php.net/get/apcu | tar xvz -C "/usr/src/php/ext/apcu" --strip 1 && \ - docker-php-ext-install apcu - APCU - - - name: Log in to GitHub Docker Registry - uses: docker/login-action@v2 - with: - registry: docker.pkg.github.com - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build container image - uses: docker/build-push-action@v3 - with: - push: true - context: 'psalm-github-actions' - file: 'psalm-github-actions/Dockerfile' - tags: | - ghcr.io/nextcloud/all-in-one-psalm:latest diff --git a/.github/workflows/dependency-updates.yml b/.github/workflows/dependency-updates.yml index bea58686..3805a0d0 100644 --- a/.github/workflows/dependency-updates.yml +++ b/.github/workflows/dependency-updates.yml @@ -1,6 +1,7 @@ name: dependency-updates on: + workflow_dispatch: schedule: - cron: '00 12 * * *' @@ -9,28 +10,27 @@ jobs: name: Run dependency update script runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: nanasess/setup-php@master + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: shivammathur/setup-php@7bf05c6b704e0b9bfee22300130a31b5ea68d593 # v2 with: - php-version: '8.0' + php-version: 8.4 + extensions: apcu - name: Run dependency update script run: | set -x - curl -sS https://getcomposer.org/installer | php - mv composer.phar /usr/local/bin/composer - chmod +x /usr/local/bin/composer cd ./php - composer update - set +e - ALL_LINES="$(composer outdated | grep -v "psr/container\|^$\|Direct dependencies\|Everything up to date\|Transitive dependencies")" - set -e - while [ -n "$ALL_LINES" ]; do - CURRENT_LINE="$(echo "$ALL_LINES" | head -1)" - composer require "$(echo "$CURRENT_LINE" | awk '{print $1}')" "^$(echo "$CURRENT_LINE" | awk '{print $4}')" - ALL_LINES="$(echo "$ALL_LINES" | sed '1d')" - done - echo "outdated dependencies: - $(composer outdated)" + composer update --with-all-dependencies + # Disable dependency updates for now + # set +e + # ALL_LINES="$(composer outdated | grep -v "^$\|Direct dependencies\|Everything up to date\|Transitive dependencies")" + # set -e + # while [ -n "$ALL_LINES" ]; do + # CURRENT_LINE="$(echo "$ALL_LINES" | head -1)" + # composer require "$(echo "$CURRENT_LINE" | awk '{print $1}')" "^$(echo "$CURRENT_LINE" | awk '{print $4}')" --with-all-dependencies + # ALL_LINES="$(echo "$ALL_LINES" | sed '1d')" + # done + # echo "outdated dependencies: + # $(composer outdated)" - name: Update apcu run: | # APCU @@ -44,12 +44,12 @@ jobs: )" sed -i "s|pecl install APCu.*\;|pecl install APCu-$apcu_version\;|" ./Containers/mastercontainer/Dockerfile - name: Create Pull Request - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7 with: - commit-message: dependency updates + commit-message: php dependency updates signoff: true - title: Dependency updates - body: Automated dependency updates since dependabot does not support grouped updates - labels: dependencies, enhancement + title: PHP dependency updates + body: Automated php dependency updates since dependabot does not support grouped updates + labels: dependencies, 3. to review milestone: next branch: aio-dependency-update diff --git a/.github/workflows/docker-lint.yml b/.github/workflows/docker-lint.yml new file mode 100644 index 00000000..3f09bb98 --- /dev/null +++ b/.github/workflows/docker-lint.yml @@ -0,0 +1,46 @@ +name: Docker Lint + +on: + pull_request: + paths: + - 'Containers/**' + push: + branches: + - main + paths: + - 'Containers/**' + +permissions: + contents: read + +concurrency: + group: docker-lint-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + docker-lint: + runs-on: ubuntu-latest + + name: docker-lint + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install hadolint + run: | + sudo wget https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -O /usr/bin/hadolint + sudo chmod +x /usr/bin/hadolint + + - name: run lint + run: | + DOCKERFILES="$(find ./Containers -name Dockerfile)" + mapfile -t DOCKERFILES <<< "$DOCKERFILES" + for file in "${DOCKERFILES[@]}"; do + # DL3018 warning: Pin versions in apk add. Instead of `apk add ` use `apk add =` + # DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check + hadolint "$file" --ignore DL3018 --ignore DL4006 | tee -a ./hadolint.log + done + if grep -q "DL[0-9]\+\|SC[0-9]\+" ./hadolint.log; then + exit 1 + fi diff --git a/.github/workflows/fail-on-prerelease.yml b/.github/workflows/fail-on-prerelease.yml new file mode 100644 index 00000000..a5b876c3 --- /dev/null +++ b/.github/workflows/fail-on-prerelease.yml @@ -0,0 +1,50 @@ +name: Block if prerelease is present + +on: + pull_request: + +permissions: + contents: read + +jobs: + check-latest-release: + runs-on: ubuntu-latest + steps: + - name: "Check latest published release isn't a prerelease" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v6 + with: + script: | + const tags = await github.rest.repos.listTags({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 1 + }); + + if (!tags.data || tags.data.length === 0) { + core.info('No tags found for this repository; skipping prerelease check.'); + return; + } + + const latestTag = tags.data[0].name; + core.info(`Latest tag found: ${latestTag}`); + + try { + const { data } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: latestTag + }); + + if (data.prerelease) { + core.setFailed(`Release for tag ${latestTag} (${data.tag_name}) is a prerelease. Blocking merges to main as we need to wait for the prerelease to become stable.`); + } else { + core.info(`Release for tag ${latestTag} (${data.tag_name}) is not a prerelease.`); + } + + } catch (err) { + if (err.status === 404) { + core.info(`No release found for tag ${latestTag}; skipping prerelease check.`); + } else { + throw err; + } + } diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml new file mode 100644 index 00000000..ba3b865d --- /dev/null +++ b/.github/workflows/helm-release.yml @@ -0,0 +1,51 @@ + +name: Helm Chart Releaser + +on: + push: + branches: + - main + paths: + - 'nextcloud-aio-helm-chart/**' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Turnstyle + uses: softprops/turnstyle@e565d2d86403c5d23533937e95980570545e5586 # v2 + with: + continue-after-seconds: 180 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Fetch history + run: git fetch --prune --unshallow + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + # See https://github.com/helm/chart-releaser-action/issues/6 + - name: Set up Helm + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 + with: + version: v3.6.3 + + - name: Run Helm Lint + run: | + helm lint ./nextcloud-aio-helm-chart + + - name: Run chart-releaser + uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0 + with: + mark_as_latest: false + charts_dir: . + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + CR_RELEASE_NAME_TEMPLATE: "helm-chart-{{ .Version }}" + CR_SKIP_EXISTING: true diff --git a/.github/workflows/imaginary-update.yml b/.github/workflows/imaginary-update.yml new file mode 100644 index 00000000..05050a20 --- /dev/null +++ b/.github/workflows/imaginary-update.yml @@ -0,0 +1,33 @@ +name: imaginary-update + +on: + workflow_dispatch: + schedule: + - cron: '00 12 * * *' + +jobs: + run_update: + name: update to latest imaginary commit on master branch + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run imaginary-update + run: | + # Imaginary + imaginary_version="$( + git ls-remote https://github.com/h2non/imaginary master \ + | cut -f1 \ + | tail -1 + )" + sed -i "s|^ENV IMAGINARY_HASH.*$|ENV IMAGINARY_HASH=$imaginary_version|" ./Containers/imaginary/Dockerfile + + - name: Create Pull Request + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7 + with: + commit-message: imaginary-update automated change + signoff: true + title: Imaginary update + body: Automated Imaginary container update + labels: dependencies, 3. to review + milestone: next + branch: imaginary-container-update diff --git a/.github/workflows/json-validator.yml b/.github/workflows/json-validator.yml index d91e47bf..8c0a7f45 100644 --- a/.github/workflows/json-validator.yml +++ b/.github/workflows/json-validator.yml @@ -1,20 +1,37 @@ -name: Json Validator - -on: - pull_request: - push: - branches: - - main - -jobs: - psalm: - name: Json Validator - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Validate Json - run: | - sudo apt install python3-pip --no-install-recommends - sudo pip3 install json-spec - json validate --schema-file=php/containers-schema.json --document-file=php/containers.json +name: Json Validator + +on: + pull_request: + paths: + - '**.json' + push: + branches: + - main + paths: + - '**.json' + +jobs: + json-validator: + name: Json Validator + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Validate Json + run: | + sudo apt-get update + sudo apt-get install python3-venv -y --no-install-recommends + python3 -m venv venv + . venv/bin/activate + pip3 install json-spec + if ! json validate --schema-file=php/containers-schema.json --document-file=php/containers.json; then + exit 1 + fi + JSON_FILES="$(find ./community-containers -name '*.json')" + mapfile -t JSON_FILES <<< "$JSON_FILES" + for file in "${JSON_FILES[@]}"; do + json validate --schema-file=php/containers-schema.json --document-file="$file" 2>&1 | tee -a ./json-validator.log + done + if grep -q "document does not validate with schema.\|invalid JSONFile" ./json-validator.log; then + exit 1 + fi diff --git a/.github/workflows/lint-helm.yml b/.github/workflows/lint-helm.yml new file mode 100644 index 00000000..61e51450 --- /dev/null +++ b/.github/workflows/lint-helm.yml @@ -0,0 +1,24 @@ +name: Lint Helm Charts + +on: + workflow_dispatch: + pull_request: + paths: + - 'nextcloud-aio-helm-chart/**' + +jobs: + lint-helm: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Install Helm + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 + with: + version: v3.11.1 + + - name: Lint charts + run: helm lint nextcloud-aio-helm-chart diff --git a/.github/workflows/lint-php.yml b/.github/workflows/lint-php.yml index 5da34471..c0d2d577 100644 --- a/.github/workflows/lint-php.yml +++ b/.github/workflows/lint-php.yml @@ -1,48 +1,67 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization - -name: Lint - -on: - pull_request: - push: - branches: - - main - - master - - stable* - -jobs: - php-lint: - runs-on: ubuntu-latest - strategy: - matrix: - php-versions: ["8.0"] - - name: php-lint - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - coverage: none - - - name: Lint - run: cd php && composer run lint - - summary: - runs-on: ubuntu-latest - needs: php-lint - - if: always() - - name: php-lint-summary - - steps: - - name: Summary status - run: if ${{ needs.php-lint.result != 'success' && needs.php-lint.result != 'skipped' }}; then exit 1; fi +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Lint php + +on: + pull_request: + paths: + - 'php/**' + push: + branches: + - main + paths: + - 'php/**' + +permissions: + contents: read + +concurrency: + group: lint-php-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + php-lint: + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: [ "8.4" ] + + name: php-lint + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@7bf05c6b704e0b9bfee22300130a31b5ea68d593 # v2.36.0 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + ini-file: development + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Lint + run: cd php && composer run lint + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: php-lint + + if: always() + + name: php-lint-summary + + steps: + - name: Summary status + run: if ${{ needs.php-lint.result != 'success' && needs.php-lint.result != 'skipped' }}; then exit 1; fi diff --git a/.github/workflows/lint-yaml.yml b/.github/workflows/lint-yaml.yml new file mode 100644 index 00000000..e36b8f4c --- /dev/null +++ b/.github/workflows/lint-yaml.yml @@ -0,0 +1,42 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Lint YAML + +on: + pull_request: + paths: + - '**.yml' + +permissions: + contents: read + +jobs: + yaml-lint: + runs-on: ubuntu-latest + + name: yaml + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.1 + with: + persist-credentials: false + + - name: GitHub action templates lint + uses: ibiqlik/action-yamllint@2576378a8e339169678f9939646ee3ee325e845c # v3.1.1 + with: + file_or_dir: .github/workflows + config_data: | + line-length: warning + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + + - name: Check GitHub actions + run: uvx zizmor --min-severity medium .github/workflows/*.yml diff --git a/.github/workflows/lock-threads.yml b/.github/workflows/lock-threads.yml index dfb1ea24..bda40ee2 100644 --- a/.github/workflows/lock-threads.yml +++ b/.github/workflows/lock-threads.yml @@ -14,7 +14,7 @@ jobs: action: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v3 + - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v5 with: issue-inactive-days: '14' process-only: 'issues' diff --git a/.github/workflows/nextcloud-update.yml b/.github/workflows/nextcloud-update.yml index aa681496..b2475290 100644 --- a/.github/workflows/nextcloud-update.yml +++ b/.github/workflows/nextcloud-update.yml @@ -2,6 +2,7 @@ name: nextcloud-update on: + workflow_dispatch: schedule: - cron: '00 12 * * *' @@ -10,7 +11,7 @@ jobs: name: Run nextcloud-update script runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run nextcloud-update script run: | # Inspired by https://github.com/nextcloud/docker/blob/master/update.sh @@ -24,7 +25,7 @@ jobs: | sort -V \ | tail -1 )" - sed -i "s|pecl install APCu.*\;|pecl install APCu-$apcu_version\;|" ./Containers/nextcloud/Dockerfile + sed -i "s|\(pecl install[^;]*APCu-\)[0-9.]*|\1$apcu_version|" ./Containers/nextcloud/Dockerfile # Memcached memcached_version="$( @@ -35,7 +36,7 @@ jobs: | sort -V \ | tail -1 )" - sed -i "s|pecl install memcached.*\;|pecl install memcached-$memcached_version\;|" ./Containers/nextcloud/Dockerfile + sed -i "s|\(pecl install[^;]*memcached-\)[0-9.]*|\1$memcached_version|" ./Containers/nextcloud/Dockerfile # Redis redis_version="$( @@ -46,31 +47,44 @@ jobs: | sort -V \ | tail -1 )" - sed -i "s|pecl install redis.*\;|pecl install redis-$redis_version\;|" ./Containers/nextcloud/Dockerfile + sed -i "s|\(pecl install[^;]*redis-\)[0-9.]*|\1$redis_version|" ./Containers/nextcloud/Dockerfile # Imagick imagick_version="$( - git ls-remote --tags https://github.com/mkoppanen/imagick.git \ + git ls-remote --tags https://github.com/imagick/imagick.git \ | cut -d/ -f3 \ | grep -viE '[a-z]' \ | tr -d '^{}' \ | sort -V \ | tail -1 )" - sed -i "s|pecl install imagick.*\;|pecl install imagick-$imagick_version\;|" ./Containers/nextcloud/Dockerfile - + sed -i "s|\(pecl install[^;]*imagick-\)[0-9.]*|\1$imagick_version|" ./Containers/nextcloud/Dockerfile + + # Igbinary + igbinary_version="$( + git ls-remote --tags https://github.com/igbinary/igbinary.git \ + | cut -d/ -f3 \ + | grep -viE '[a-z]' \ + | tr -d '^{}' \ + | sort -V \ + | tail -1 + )" + sed -i "s|\(pecl install[^;]*igbinary-\)[0-9.]*|\1$igbinary_version|" ./Containers/nextcloud/Dockerfile + # Nextcloud NC_MAJOR="$(grep "ENV NEXTCLOUD_VERSION" ./Containers/nextcloud/Dockerfile | grep -oP '[23][0-9]')" NCVERSION=$(curl -s -m 900 https://download.nextcloud.com/server/releases/ | sed --silent 's/.*href="nextcloud-\([^"]\+\).zip.asc".*/\1/p' | grep "$NC_MAJOR" | sort --version-sort | tail -1) - sed -i "s|^ENV NEXTCLOUD_VERSION.*|ENV NEXTCLOUD_VERSION $NCVERSION|" ./Containers/nextcloud/Dockerfile + if [ -n "$NCVERSION" ]; then + sed -i "s|^ENV NEXTCLOUD_VERSION.*|ENV NEXTCLOUD_VERSION=$NCVERSION|" ./Containers/nextcloud/Dockerfile + fi - name: Create Pull Request - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7 with: commit-message: nextcloud-update automated change signoff: true - title: Nextcloud update + title: Nextcloud dependency update body: Automated Nextcloud container update - labels: dependencies, enhancement + labels: dependencies, 3. to review milestone: next branch: nextcloud-container-update diff --git a/.github/workflows/php-deprecation-detector.yml b/.github/workflows/php-deprecation-detector.yml new file mode 100644 index 00000000..38b0fa8d --- /dev/null +++ b/.github/workflows/php-deprecation-detector.yml @@ -0,0 +1,35 @@ +name: PHP Deprecation Detector +# See https://github.com/wapmorgan/PhpDeprecationDetector + +on: + pull_request: + paths: + - 'php/**' + push: + branches: + - main + paths: + - 'php/**' + +jobs: + phpdd: + name: PHP Deprecation Detector + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up php + uses: shivammathur/setup-php@7bf05c6b704e0b9bfee22300130a31b5ea68d593 # v2 + with: + php-version: 8.4 + extensions: apcu + coverage: none + + - name: Run script + run: | + set -x + cd php + composer install + composer run php-deprecation-detector | tee -i ./phpdd.log + if grep "Total issues:" ./phpdd.log; then + exit 1 + fi diff --git a/.github/workflows/playwright-on-push.yml b/.github/workflows/playwright-on-push.yml new file mode 100644 index 00000000..40277e57 --- /dev/null +++ b/.github/workflows/playwright-on-push.yml @@ -0,0 +1,123 @@ +name: Playwright Tests on push + +on: + pull_request: + paths: + - 'php/**' + push: + branches: + - main + paths: + - 'php/**' + +concurrency: + group: playwright-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + BASE_URL: https://localhost:8080 + +jobs: + test: + timeout-minutes: 60 + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 + with: + node-version: lts/* + + - name: Install dependencies + run: cd php/tests && npm ci + + - name: Install Playwright Browsers + run: cd php/tests && npx playwright install --with-deps chromium + + - name: Set up php 8.4 + uses: shivammathur/setup-php@7bf05c6b704e0b9bfee22300130a31b5ea68d593 # v2.36.0 + with: + extensions: apcu + php-version: 8.4 + coverage: none + ini-file: development + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Adjust some things and fix permissions + run: | + cd php + rm -r ./data + rm -r ./session + composer install --no-dev + composer clear-cache + sudo chmod 777 -R ./ + + - name: Start fresh development server + run: | + docker rm --force nextcloud-aio-{mastercontainer,apache,notify-push,nextcloud,redis,database,domaincheck,whiteboard,imaginary,talk,collabora,borgbackup} || true + docker volume rm nextcloud_aio_{mastercontainer,apache,database,database_dump,nextcloud,nextcloud_data,redis,backup_cache,elasticsearch} || true + docker pull ghcr.io/nextcloud-releases/all-in-one:develop + docker run \ + -d \ + --init \ + --name nextcloud-aio-mastercontainer \ + --restart always \ + --publish 8080:8080 \ + --volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ + --volume ./php:/var/www/docker-aio/php \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + --env SKIP_DOMAIN_VALIDATION=true \ + --env APACHE_PORT=11000 \ + ghcr.io/nextcloud-releases/all-in-one:develop + echo Waiting for 10 seconds for the development container to start ... + sleep 10 + + - name: Run Playwright tests for initial setup + run: | + cd php/tests + export DEBUG=pw:api + if ! npx playwright test tests/initial-setup.spec.js; then + docker logs nextcloud-aio-mastercontainer + docker logs nextcloud-aio-borgbackup + exit 1 + fi + + - name: Start fresh development server + run: | + docker rm --force nextcloud-aio-{mastercontainer,apache,notify-push,nextcloud,redis,database,domaincheck,whiteboard,imaginary,talk,collabora,borgbackup} || true + docker volume rm nextcloud_aio_{mastercontainer,apache,database,database_dump,nextcloud,nextcloud_data,redis,backup_cache,elasticsearch} || true + docker run \ + -d \ + --init \ + --name nextcloud-aio-mastercontainer \ + --restart always \ + --publish 8080:8080 \ + --volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ + --volume ./php:/var/www/docker-aio/php \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + --env SKIP_DOMAIN_VALIDATION=false \ + --env APACHE_PORT=11000 \ + ghcr.io/nextcloud-releases/all-in-one:develop + echo Waiting for 10 seconds for the development container to start ... + sleep 10 + + - name: Run Playwright tests for backup restore + run: | + cd php/tests + export DEBUG=pw:api + if ! npx playwright test tests/restore-instance.spec.js; then + docker logs nextcloud-aio-mastercontainer + docker logs nextcloud-aio-borgbackup + exit 1 + fi + + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: php/tests/playwright-report/ + retention-days: 14 + overwrite: true diff --git a/.github/workflows/playwright-on-workflow-dispatch.yml b/.github/workflows/playwright-on-workflow-dispatch.yml new file mode 100644 index 00000000..6d2f6d32 --- /dev/null +++ b/.github/workflows/playwright-on-workflow-dispatch.yml @@ -0,0 +1,91 @@ +name: Playwright Tests + +on: + workflow_dispatch: + +env: + BASE_URL: https://localhost:8080 + +jobs: + test: + timeout-minutes: 60 + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 + with: + node-version: lts/* + + - name: Install dependencies + run: cd php/tests && npm ci + + - name: Install Playwright Browsers + run: cd php/tests && npx playwright install --with-deps chromium + + - name: Start fresh development server + run: | + docker rm --force nextcloud-aio-{mastercontainer,apache,notify-push,nextcloud,redis,database,domaincheck,whiteboard,imaginary,talk,collabora,borgbackup} || true + docker volume rm nextcloud_aio_{mastercontainer,apache,database,database_dump,nextcloud,nextcloud_data,redis,backup_cache,elasticsearch} || true + docker pull ghcr.io/nextcloud-releases/all-in-one:develop + docker run \ + -d \ + --init \ + --name nextcloud-aio-mastercontainer \ + --restart always \ + --publish 8080:8080 \ + --volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + --env SKIP_DOMAIN_VALIDATION=true \ + --env APACHE_PORT=11000 \ + ghcr.io/nextcloud-releases/all-in-one:develop + echo Waiting for 10 seconds for the development container to start ... + sleep 10 + + - name: Run Playwright tests for initial setup + run: | + cd php/tests + export DEBUG=pw:api + if ! npx playwright test tests/initial-setup.spec.js; then + docker logs nextcloud-aio-mastercontainer + docker logs nextcloud-aio-borgbackup + exit 1 + fi + + - name: Start fresh development server + run: | + docker rm --force nextcloud-aio-{mastercontainer,apache,notify-push,nextcloud,redis,database,domaincheck,whiteboard,imaginary,talk,collabora,borgbackup} || true + docker volume rm nextcloud_aio_{mastercontainer,apache,database,database_dump,nextcloud,nextcloud_data,redis,backup_cache,elasticsearch} || true + docker run \ + -d \ + --init \ + --name nextcloud-aio-mastercontainer \ + --restart always \ + --publish 8080:8080 \ + --volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + --env SKIP_DOMAIN_VALIDATION=false \ + --env APACHE_PORT=11000 \ + ghcr.io/nextcloud-releases/all-in-one:develop + echo Waiting for 10 seconds for the development container to start ... + sleep 10 + + - name: Run Playwright tests for backup restore + run: | + cd php/tests + export DEBUG=pw:api + if ! npx playwright test tests/restore-instance.spec.js; then + docker logs nextcloud-aio-mastercontainer + docker logs nextcloud-aio-borgbackup + exit 1 + fi + + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: php/tests/playwright-report/ + retention-days: 14 + overwrite: true diff --git a/.github/workflows/psalm-analysis.yml b/.github/workflows/psalm-analysis.yml deleted file mode 100644 index 0a5a42ad..00000000 --- a/.github/workflows/psalm-analysis.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Psalm Analysis - -on: - pull_request: - push: - branches: - - main - -jobs: - psalm: - name: Psalm - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up php8.0 - uses: shivammathur/setup-php@v2 - with: - php-version: 8.0 - extensions: apcu - coverage: none - - - name: Run script - run: | - set -x - cd php - composer global require vimeo/psalm --prefer-dist --no-progress --dev - composer install - composer run psalm diff --git a/.github/workflows/psalm-security.yml b/.github/workflows/psalm-security.yml deleted file mode 100644 index 1dd39fe8..00000000 --- a/.github/workflows/psalm-security.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Psalm Security Analysis - -on: - push: - branches: - - main - -jobs: - psalm: - name: Psalm - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Psalm - uses: docker://ghcr.io/nextcloud/all-in-one-psalm - with: - relative_dir: php - security_analysis: true - composer_ignore_platform_reqs: false - report_file: results.sarif - - name: Upload Security Analysis results to GitHub - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: php/results.sarif diff --git a/.github/workflows/psalm-update-baseline.yml b/.github/workflows/psalm-update-baseline.yml index 8492f071..bcbb12c3 100644 --- a/.github/workflows/psalm-update-baseline.yml +++ b/.github/workflows/psalm-update-baseline.yml @@ -10,12 +10,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up php8.0 - uses: shivammathur/setup-php@v2 + - name: Set up php + uses: shivammathur/setup-php@7bf05c6b704e0b9bfee22300130a31b5ea68d593 # v2 with: - php-version: 8.0 + php-version: 8.4 extensions: apcu coverage: none @@ -23,15 +23,14 @@ jobs: run: | set -x cd php - composer global require vimeo/psalm --prefer-dist --no-progress --dev composer install - composer run psalm -- --monochrome --no-progress --output-format=text --update-baseline + composer run psalm:update-baseline git clean -f lib/composer git checkout composer.json composer.lock lib/composer continue-on-error: true - name: Create Pull Request - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7 with: token: ${{ secrets.COMMAND_BOT_PAT }} commit-message: Update psalm baseline @@ -39,10 +38,9 @@ jobs: author: nextcloud-command signoff: true branch: automated/noid/psalm-baseline-update - # Make sure we can open multiple PRs - branch-suffix: timestamp title: '[Automated] Update psalm-baseline.xml' + milestone: next body: | Auto-generated update psalm-baseline.xml with fixed psalm warnings labels: | - 3. to review \ No newline at end of file + 3. to review, dependencies diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml new file mode 100644 index 00000000..2bab876e --- /dev/null +++ b/.github/workflows/psalm.yml @@ -0,0 +1,56 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Static analysis + +on: + pull_request: + paths: + - 'php/**' + push: + branches: + - main + paths: + - 'php/**' + +concurrency: + group: psalm-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + static-analysis: + runs-on: ubuntu-latest + + name: static-psalm-analysis + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up php + uses: shivammathur/setup-php@7bf05c6b704e0b9bfee22300130a31b5ea68d593 # v2.36.0 + with: + php-version: 8.4 + extensions: apcu + coverage: none + ini-file: development + # Temporary workaround for missing pcntl_* in PHP 8.3 + ini-values: disable_functions= + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies and run psalm + run: | + set -x + cd php + composer install + composer run psalm diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 9b80cf3f..b051c355 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -2,18 +2,22 @@ name: Shellcheck on: pull_request: + paths: + - '**.sh' push: branches: - main + paths: + - '**.sh' jobs: shellcheck: - name: Github Actions + name: Check Shell runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Shellcheck - uses: ludeeus/action-shellcheck@master + uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # v2.0.0 with: check_together: 'yes' env: diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml deleted file mode 100644 index 49c9b116..00000000 --- a/.github/workflows/spellcheck.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: 'Spellcheck' - -on: - pull_request: - push: - branches: - - main - -jobs: - spellcheck: - name: Check spelling - runs-on: ubuntu-latest - steps: - - name: spelling or typos - uses: actions/checkout@v3 - - name: fix permission for reviewdog - run: sudo chown -R root:root $GITHUB_WORKSPACE - - name: misspell - uses: reviewdog/action-misspell@v1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - locale: "US" - fail_on_error: true diff --git a/.github/workflows/talk.yml b/.github/workflows/talk.yml new file mode 100644 index 00000000..b19e1cb5 --- /dev/null +++ b/.github/workflows/talk.yml @@ -0,0 +1,56 @@ +name: talk-update + +on: + workflow_dispatch: + schedule: + - cron: '00 12 * * *' + +jobs: + talk-update: + name: update talk + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run talk-container-update + run: | + # Recording + recording_version="$( + git ls-remote https://github.com/nextcloud/nextcloud-talk-recording v* \ + | cut -d/ -f3 \ + | sort -V \ + | grep -E "^v[0-9\.]+$" \ + | tail -1 + )" + sed -i "s|^ENV RECORDING_VERSION.*$|ENV RECORDING_VERSION=$recording_version|" ./Containers/talk-recording/Dockerfile + curl -L "https://raw.githubusercontent.com/nextcloud/nextcloud-talk-recording/$recording_version/server.conf.in" -o Containers/talk-recording/recording.conf + + # Signaling + signaling_version="$( + git ls-remote https://github.com/strukturag/nextcloud-spreed-signaling v*.*.* \ + | cut -d/ -f3 \ + | sort -V \ + | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$" \ + | tail -1 + )" + curl -L "https://raw.githubusercontent.com/strukturag/nextcloud-spreed-signaling/$signaling_version/server.conf.in" -o Containers/talk/server.conf.in + + # Janus + janus_version="$( + git ls-remote https://github.com/meetecho/janus-gateway v1.*.* \ + | cut -d/ -f3 \ + | sort -V \ + | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$" \ + | tail -1 + )" + sed -i "s|^ARG JANUS_VERSION=.*$|ARG JANUS_VERSION=$janus_version|" ./Containers/talk/Dockerfile + + - name: Create Pull Request + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7 + with: + commit-message: talk-update automated change + signoff: true + title: talk container update + body: Automated talk container update + labels: dependencies, 3. to review + milestone: next + branch: talk-container-update diff --git a/.github/workflows/twig-lint.yml b/.github/workflows/twig-lint.yml new file mode 100644 index 00000000..27b8776d --- /dev/null +++ b/.github/workflows/twig-lint.yml @@ -0,0 +1,40 @@ +name: Twig Lint + +on: + pull_request: + paths: + - '**.twig' + push: + branches: + - main + paths: + - '**.twig' + +permissions: + contents: read + +concurrency: + group: lint-twig-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + twig-lint: + runs-on: ubuntu-latest + name: twig-lint + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@7bf05c6b704e0b9bfee22300130a31b5ea68d593 # v2 + with: + php-version: 8.4 + extensions: apcu + coverage: none + + - name: twig lint + run: | + cd php + composer install + composer run lint:twig diff --git a/.github/workflows/update-copyright.yml b/.github/workflows/update-copyright.yml new file mode 100644 index 00000000..103851c9 --- /dev/null +++ b/.github/workflows/update-copyright.yml @@ -0,0 +1,11 @@ +name: Update Copyright + +on: + workflow_dispatch: + +jobs: + update-copyright: + name: update copyright + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/update-helm.yml b/.github/workflows/update-helm.yml new file mode 100644 index 00000000..92cbb978 --- /dev/null +++ b/.github/workflows/update-helm.yml @@ -0,0 +1,35 @@ +name: Update Helm Chart + +on: + workflow_dispatch: + schedule: + - cron: '00 12 * * *' + +jobs: + update-helm: + name: update helm chart + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: update helm chart + run: | + set -x + GHCR_TOKEN="$(curl https://ghcr.io/token?scope=repository:nextcloud-releases/nce-php-fpm-mgmt:pull | jq '.token' | sed 's|"||g')" + DOCKER_TAG="$(curl -H "Authorization: Bearer ${GHCR_TOKEN}" -L -s 'https://ghcr.io/v2/nextcloud-releases/all-in-one/tags/list?page_size=1024' | jq '.tags' | sed 's|"||g;s|[[:space:]]||g;s|,||g' | grep '^20[0-9_]\+' | grep -v latest | sort -r | head -1)" + export DOCKER_TAG + set +x + if [ -n "$DOCKER_TAG" ] && ! grep -q "aio-nextcloud:$DOCKER_TAG" ./nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-deployment.yaml; then + sudo bash nextcloud-aio-helm-chart/update-helm.sh "$DOCKER_TAG" + fi + - name: Create Pull Request + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7 + with: + commit-message: Helm Chart updates + signoff: true + title: Helm Chart updates + body: Automated Helm Chart updates for the yaml files. It can be merged if it looks good at any time which will automatically trigger a new release of the helm chart. + labels: dependencies, 3. to review + milestone: next + branch: aio-helm-update + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-yaml.yml b/.github/workflows/update-yaml.yml index 48f5ed46..6e150261 100644 --- a/.github/workflows/update-yaml.yml +++ b/.github/workflows/update-yaml.yml @@ -6,22 +6,23 @@ on: - cron: '00 12 * * *' jobs: - psalm: + update-yaml: name: update yaml files runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: update yaml files run: | sudo bash manual-install/update-yaml.sh - name: Create Pull Request - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7 with: commit-message: Yaml updates signoff: true title: Yaml updates body: Automated yaml updates for the docker-compose files. Should only be merged shortly before the next latest release. - labels: dependencies + labels: dependencies, 3. to review milestone: next branch: aio-yaml-update + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/watchtower-update.yml b/.github/workflows/watchtower-update.yml new file mode 100644 index 00000000..ecd82a69 --- /dev/null +++ b/.github/workflows/watchtower-update.yml @@ -0,0 +1,37 @@ +name: watchtower-update + +on: + workflow_dispatch: + schedule: + - cron: '00 12 * * *' + +jobs: + watchtower-update: + name: update watchtower + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run watchtower-container-update + run: | + # Watchtower + watchtower_version="$( + git ls-remote https://github.com/nicholas-fedor/watchtower v* \ + | cut -d/ -f3 \ + | sort -V \ + | grep -E "^v[0-9\.]+$" \ + | tail -1 + )" + watchtower_commit_hash="$(git ls-remote https://github.com/nicholas-fedor/watchtower $watchtower_version | sed 's/refs.*//')" + sed -i "s|^ENV WATCHTOWER_COMMIT_HASH.*$|ENV WATCHTOWER_COMMIT_HASH=$watchtower_commit_hash|" ./Containers/watchtower/Dockerfile + sed -i "s|\$WATCHTOWER_COMMIT_HASH.*$|\$WATCHTOWER_COMMIT_HASH # $watchtower_version|" ./Containers/watchtower/Dockerfile + + - name: Create Pull Request + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7 + with: + commit-message: watchtower-update automated change + signoff: true + title: watchtower container update + body: Automated watchtower container update + labels: dependencies, 3. to review + milestone: next + branch: watchtower-container-update diff --git a/.gitignore b/.gitignore index a27e8815..a9ac8d70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,15 @@ .DS_Store -/php/data/containers.json -/php/data/configuration.json -/php/data/backupsecret.json +.idea/ +*.iml + +/php/data/* +/php/session/* +!/php/data/.gitkeep +!/php/session/.gitkeep /php/vendor + /manual-install/*.conf !/manual-install/sample.conf -/manual-install/docker-compose.yml \ No newline at end of file +/manual-install/docker-compose.yml +/manual-install/compose.yaml +/manual-install/.env diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 37219f9c..00000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @szaimen @juliushaertl diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..fec85a59 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ + +In the Nextcloud community, participants from all over the world come together to create Free Software for a free internet. This is made possible by the support, hard work and enthusiasm of thousands of people, including those who create and use Nextcloud software. + +Our code of conduct offers some guidance to ensure Nextcloud participants can cooperate effectively in a positive and inspiring atmosphere, and to explain how together we can strengthen and support each other. + +The Code of Conduct is shared by all contributors and users who engage with the Nextcloud team and its community services. It presents a summary of the shared values and “common sense” thinking in our community. + +You can find our full code of conduct on our website: https://nextcloud.com/code-of-conduct/ + +Please, keep our CoC in mind when you contribute! That way, everyone can be a part of our community in a productive, positive, creative and fun way. diff --git a/Containers/alpine/Dockerfile b/Containers/alpine/Dockerfile new file mode 100644 index 00000000..1098b4c4 --- /dev/null +++ b/Containers/alpine/Dockerfile @@ -0,0 +1,7 @@ +# syntax=docker/dockerfile:latest +FROM alpine:3.23.3 + +RUN set -ex; \ + apk upgrade --no-cache -a + +LABEL org.label-schema.vendor="Nextcloud" diff --git a/Containers/apache/Caddyfile b/Containers/apache/Caddyfile index 461c0389..4b92d807 100644 --- a/Containers/apache/Caddyfile +++ b/Containers/apache/Caddyfile @@ -4,63 +4,67 @@ storage file_system { root /mnt/data/caddy } + + servers { + # trusted_proxies placeholder + } + + log { + level ERROR + } } +https://{$ADDITIONAL_TRUSTED_DOMAIN}:443, +http://{$APACHE_HOST}:23973, # For Collabora callback and WOPI requests, see containers.json {$PROTOCOL}://{$NC_DOMAIN}:{$APACHE_PORT} { + header -Server + header -X-Powered-By + + # Collabora + route /browser/* { + reverse_proxy {$COLLABORA_HOST}:9980 + } + route /hosting/* { + reverse_proxy {$COLLABORA_HOST}:9980 + } + route /cool/* { + reverse_proxy {$COLLABORA_HOST}:9980 + } # Notify Push route /push/* { uri strip_prefix /push - reverse_proxy {$NEXTCLOUD_HOST}:7867 { - # trusted_proxies placeholder - } - } - - # Talk - route /standalone-signaling/* { - uri strip_prefix /standalone-signaling - reverse_proxy {$TALK_HOST}:8081 { - # trusted_proxies placeholder - } - } - - # Collabora - route /browser/* { - reverse_proxy {$COLLABORA_HOST}:9980 { - # trusted_proxies placeholder - } - } - route /hosting/* { - reverse_proxy {$COLLABORA_HOST}:9980 { - # trusted_proxies placeholder - } - } - route /cool/* { - reverse_proxy {$COLLABORA_HOST}:9980 { - # trusted_proxies placeholder - } + reverse_proxy {$NOTIFY_PUSH_HOST}:7867 } # Onlyoffice route /onlyoffice/* { uri strip_prefix /onlyoffice reverse_proxy {$ONLYOFFICE_HOST}:80 { - header_up X-Forwarded-Host {http.request.host}/onlyoffice + header_up X-Forwarded-Host {http.request.hostport}/onlyoffice header_up X-Forwarded-Proto https - # trusted_proxies placeholder } } + # Talk + route /standalone-signaling/* { + uri strip_prefix /standalone-signaling + reverse_proxy {$TALK_HOST}:8081 + } + + # Whiteboard + route /whiteboard/* { + uri strip_prefix /whiteboard + reverse_proxy {$WHITEBOARD_HOST}:3002 + } + # Nextcloud route { - rewrite /.well-known/carddav /remote.php/dav - rewrite /.well-known/caldav /remote.php/dav header Strict-Transport-Security max-age=31536000; - reverse_proxy localhost:8000 { - # See https://github.com/nextcloud/all-in-one/issues/828 - # trusted_proxies placeholder - } + reverse_proxy 127.0.0.1:8000 } + redir /.well-known/carddav /remote.php/dav/ 301 + redir /.well-known/caldav /remote.php/dav/ 301 # TLS options tls { diff --git a/Containers/apache/Dockerfile b/Containers/apache/Dockerfile index 22576972..9ccadfb8 100644 --- a/Containers/apache/Dockerfile +++ b/Containers/apache/Dockerfile @@ -1,80 +1,92 @@ -# Caddy is a requirement -FROM caddy:2.5.2-alpine as caddy +# syntax=docker/dockerfile:latest +FROM caddy:2.10.2-alpine AS caddy -FROM debian:bullseye-20220822-slim +# From https://github.com/docker-library/httpd/blob/master/2.4/alpine/Dockerfile +FROM httpd:2.4.66-alpine3.23 -RUN mkdir -p /mnt/data; \ - chown www-data:www-data /mnt/data; +COPY --from=caddy /usr/bin/caddy /usr/bin/caddy + +COPY --chown=33:33 Caddyfile /Caddyfile +COPY --chmod=664 nextcloud.conf /usr/local/apache2/conf/nextcloud.conf +COPY --chmod=664 supervisord.conf /supervisord.conf +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh VOLUME /mnt/data RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache shadow; \ + groupmod -g 33 www-data; \ + usermod -u 33 -g 33 www-data; \ + apk del --no-cache shadow; \ \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - apache2 \ + mkdir -p /mnt/data; \ + chown -R www-data:www-data /mnt/data; \ + chown -R 777 /tmp; \ + \ + apk add --no-cache \ + bash \ supervisor \ - wget \ + tzdata \ ca-certificates \ openssl \ - netcat \ - dpkg-dev \ - curl \ - ; \ - rm -rf /var/lib/apt/lists/* - -COPY --from=caddy /usr/bin/caddy /usr/bin/ -RUN chmod +x /usr/bin/caddy - -RUN a2enmod rewrite \ - headers \ - proxy \ - proxy_fcgi \ - setenvif \ - env \ - mime \ - dir \ - authz_core \ - alias - -COPY nextcloud.conf /etc/apache2/sites-available/ - -RUN rm /etc/apache2/ports.conf; \ - sed -s -i -e "s/Include ports.conf//" /etc/apache2/apache2.conf; \ - sed -i "/^Listen /d" /etc/apache2/apache2.conf - -RUN set -ex; \ - a2dissite 000-default && \ - a2dissite default-ssl && \ - a2ensite nextcloud.conf && \ - rm -rf /var/www/html/* && \ - chown www-data:www-data -R /var/log/apache2; \ - mkdir -p /var/run/apache2; \ - chown -R www-data:www-data /var/run/apache2; \ - chown -R www-data:www-data /var/www; - -RUN mkdir /var/log/supervisord; \ + bind-tools \ + netcat-openbsd; \ + \ + sed -i \ + -e '/^Listen /d' \ + -e 's/^#\(LoadModule .*mod_rewrite.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_headers.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_proxy.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_proxy_fcgi.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_setenvif.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_env.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_mime.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_dir.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_authz_core.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_alias.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_mpm_event.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_brotli.so\)/\1/' \ + -e 's/\(LoadModule .*mod_mpm_worker.so\)/#\1/' \ + -e 's/\(LoadModule .*mod_mpm_prefork.so\)/#\1/' \ + -e 's/\(ScriptAlias \)/#\1/' \ + /usr/local/apache2/conf/httpd.conf; \ + echo "Include conf/nextcloud.conf" | tee -a /usr/local/apache2/conf/httpd.conf; \ + echo "ServerName localhost" | tee -a /usr/local/apache2/conf/httpd.conf; \ +# Sync this with max db connections and pm.max_children +# We don't actually expect so many workers but don't want to limit it artificially because people will report issues otherwise. + sed -i 's|MaxRequestWorkers.*|MaxRequestWorkers 5000|' /usr/local/apache2/conf/extra/httpd-mpm.conf; \ + grep -q '' /usr/local/apache2/conf/extra/httpd-mpm.conf; \ +# ServerLimit needs to be set to MaxRequestWorkers divided by ThreadsPerChild which is set to 25 by default + sed -i '//a\ \ \ \ ServerLimit 200' /usr/local/apache2/conf/extra/httpd-mpm.conf; \ + \ + rm -rf /usr/local/apache2/conf/original /var/www; \ + mkdir -p /var/www; \ + chown -R www-data:www-data /var/www; \ + \ + mkdir /var/log/supervisord; \ mkdir /var/run/supervisord; \ chown www-data:www-data /var/run/supervisord; \ - chown www-data:www-data /var/log/supervisord; + chown www-data:www-data /var/log/supervisord; \ + chmod 777 /var/run/supervisord; \ + chmod 777 /var/log/supervisord; \ + \ + chown -R www-data:www-data /usr/local/apache2; \ + chmod +r -R /usr/local/apache2; \ + mkdir -p /usr/local/apache2/logs; \ + chmod 777 -R /home/www-data; \ + chmod 777 -R /usr/local/apache2/logs; \ + rm -rf /usr/local/apache2/cgi-bin/; \ + \ + echo "root:$(openssl rand -base64 12)" | chpasswd -COPY Caddyfile / +USER 33 -COPY start.sh /usr/bin/ -COPY healthcheck.sh /usr/bin/ -COPY supervisord.conf / -RUN chmod +x /usr/bin/start.sh; \ - chmod +x /usr/bin/healthcheck.sh; \ - chmod +r /supervisord.conf; \ - chown www-data:www-data /Caddyfile; \ - chmod +r -R /etc/apache2 - -# Give root a random password -RUN echo "root:$(openssl rand -base64 12)" | chpasswd - -USER www-data - -ENTRYPOINT ["start.sh"] +ENTRYPOINT ["/start.sh"] CMD ["/usr/bin/supervisord", "-c", "/supervisord.conf"] -HEALTHCHECK CMD healthcheck.sh \ No newline at end of file +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/apache/healthcheck.sh b/Containers/apache/healthcheck.sh index b11d8b5b..e9c1fad4 100644 --- a/Containers/apache/healthcheck.sh +++ b/Containers/apache/healthcheck.sh @@ -1,8 +1,5 @@ #!/bin/bash -curl -skfI localhost:8000 || exit 1 -if [ "$APACHE_PORT" != '443' ]; then - curl -skfI localhost:"$APACHE_PORT" || exit 1 -else - curl -skfI https://"$NC_DOMAIN":"$APACHE_PORT" || exit 1 -fi +nc -z "$NEXTCLOUD_HOST" 9000 || exit 0 +nc -z 127.0.0.1 8000 || exit 1 +nc -z 127.0.0.1 "$APACHE_PORT" || exit 1 diff --git a/Containers/apache/nextcloud.conf b/Containers/apache/nextcloud.conf index d701a264..728d19dc 100644 --- a/Containers/apache/nextcloud.conf +++ b/Containers/apache/nextcloud.conf @@ -1,9 +1,28 @@ Listen 8000 + ServerName localhost + + # Add error log + CustomLog /proc/self/fd/1 proxy + LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" proxy + ErrorLog /proc/self/fd/2 + ErrorLogFormat "[%t] [%l] [%E] [client: %{X-Forwarded-For}i] [%M] [%{User-Agent}i]" + LogLevel warn + # PHP match - SetHandler "proxy:fcgi://nextcloud-aio-nextcloud:9000" + SetHandler "proxy:fcgi://${NEXTCLOUD_HOST}:9000" + + + + + # Enable Brotli compression for js, css and svg files - other plain files are compressed by Nextcloud by default + + AddOutputFilterByType BROTLI_COMPRESS text/javascript application/javascript application/x-javascript text/css image/svg+xml + BrotliCompressionQuality 0 + + # Nextcloud dir DocumentRoot /var/www/html/ @@ -21,10 +40,15 @@ Listen 8000 Require all denied - # Fix zero file sizes - # See https://github.com/nextcloud/server/issues/3056#issuecomment-954209565 - SetEnv proxy-sendcl 1 - # See https://httpd.apache.org/docs/current/en/mod/core.html#limitrequestbody - LimitRequestBody 0 + LimitRequestBody ${APACHE_MAX_SIZE} + + # See https://httpd.apache.org/docs/current/mod/core.html#timeout + Timeout ${APACHE_MAX_TIME} + + # See https://httpd.apache.org/docs/current/mod/mod_proxy.html#proxytimeout + ProxyTimeout ${APACHE_MAX_TIME} + + # See https://httpd.apache.org/docs/trunk/mod/core.html#traceenable + TraceEnable Off diff --git a/Containers/apache/start.sh b/Containers/apache/start.sh index bed49656..02a2f2ad 100644 --- a/Containers/apache/start.sh +++ b/Containers/apache/start.sh @@ -17,6 +17,13 @@ while ! nc -z "$NEXTCLOUD_HOST" 9000; do sleep 5 done +# Get ipv4-address of Apache +# shellcheck disable=SC2153 +IPv4_ADDRESS="$(dig "$APACHE_HOST" A +short +search | head -1)" +# Bring it in CIDR notation +# shellcheck disable=SC2001 +IPv4_ADDRESS="$(echo "$IPv4_ADDRESS" | sed 's|[0-9]\+$|0/16|')" + if [ -z "$APACHE_PORT" ]; then export APACHE_PORT="443" fi @@ -35,20 +42,36 @@ if [ "$APACHE_PORT" != '443' ]; then else CADDYFILE="$(sed 's|auto_https.*|auto_https disable_redirects|' /Caddyfile)" fi -echo "$CADDYFILE" > /Caddyfile +echo "$CADDYFILE" > /tmp/Caddyfile # Change the trusted_proxies in case of reverse proxies if [ "$APACHE_PORT" != '443' ]; then - CADDYFILE="$(sed 's|# trusted_proxies placeholder|trusted_proxies private_ranges|' /Caddyfile)" + # Here the 100.64.0.0/10 range gets added which is the CGNAT range used by Tailscale nodes + # See https://github.com/nextcloud/all-in-one/pull/6703 for reference + CADDYFILE="$(sed 's|# trusted_proxies placeholder|trusted_proxies static private_ranges 100.64.0.0/10|' /tmp/Caddyfile)" else - CADDYFILE="$(sed 's|trusted_proxies private_ranges|# trusted_proxies placeholder|' /Caddyfile)" + CADDYFILE="$(sed "s|# trusted_proxies placeholder|trusted_proxies static $IPv4_ADDRESS|" /tmp/Caddyfile)" fi -echo "$CADDYFILE" > /Caddyfile +echo "$CADDYFILE" > /tmp/Caddyfile + +# Remove additional domain if not given +if [ -z "$ADDITIONAL_TRUSTED_DOMAIN" ]; then + CADDYFILE="$(sed '/ADDITIONAL_TRUSTED_DOMAIN/d' /tmp/Caddyfile)" +fi +echo "$CADDYFILE" > /tmp/Caddyfile + +# Fix the Caddyfile format +caddy fmt --overwrite /tmp/Caddyfile # Add caddy path mkdir -p /mnt/data/caddy/ -# Fix apache sturtup -rm -f /var/run/apache2/apache2.pid +# Fix caddy startup +if [ -d "/mnt/data/caddy/locks" ]; then + rm -rf /mnt/data/caddy/locks/* +fi -exec "$@" \ No newline at end of file +# Fix apache startup +rm -f /usr/local/apache2/logs/httpd.pid + +exec "$@" diff --git a/Containers/apache/supervisord.conf b/Containers/apache/supervisord.conf index 0abc9748..7ab935e4 100644 --- a/Containers/apache/supervisord.conf +++ b/Containers/apache/supervisord.conf @@ -1,23 +1,23 @@ -[supervisord] -nodaemon=true -nodaemon=true -logfile=/var/log/supervisord/supervisord.log -pidfile=/var/run/supervisord/supervisord.pid -childlogdir=/var/log/supervisord/ -logfile_maxbytes=50MB -logfile_backups=10 -loglevel=error - -[program:apache] -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -command=apachectl -DFOREGROUND - -[program:caddy] -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -command=/usr/bin/caddy run -config /Caddyfile \ No newline at end of file +[supervisord] +nodaemon=true +nodaemon=true +logfile=/var/log/supervisord/supervisord.log +pidfile=/var/run/supervisord/supervisord.pid +childlogdir=/var/log/supervisord/ +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=error + +[program:apache] +# Stdout logging is disabled as otherwise the logs are spammed +stdout_logfile=NONE +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=apachectl -DFOREGROUND + +[program:caddy] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=/usr/bin/caddy run --config /tmp/Caddyfile diff --git a/Containers/borgbackup/Dockerfile b/Containers/borgbackup/Dockerfile index ef3b89a7..6e3180cb 100644 --- a/Containers/borgbackup/Dockerfile +++ b/Containers/borgbackup/Dockerfile @@ -1,23 +1,29 @@ -FROM debian:bullseye-20220822-slim +# syntax=docker/dockerfile:latest +FROM alpine:3.23.3 RUN set -ex; \ \ - apt-get update; \ - apt-get install -y --no-install-recommends \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + util-linux-misc \ + bash \ borgbackup \ rsync \ fuse \ - python3-llfuse \ + py3-llfuse \ jq \ - ; \ - rm -rf /var/lib/apt/lists/* + openssh-client VOLUME /root -COPY start.sh /usr/bin/ -COPY backupscript.sh / -RUN chmod +x /usr/bin/start.sh; \ - chmod +x /backupscript.sh +COPY --chmod=770 *.sh / +COPY borg_excludes / +ENTRYPOINT ["/start.sh"] +# hadolint ignore=DL3002 USER root -ENTRYPOINT ["start.sh"] \ No newline at end of file + +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" +ENV BORG_RETENTION_POLICY="--keep-within=7d --keep-weekly=4 --keep-monthly=6" diff --git a/Containers/borgbackup/backupscript.sh b/Containers/borgbackup/backupscript.sh index c6b07d17..b7b96147 100644 --- a/Containers/borgbackup/backupscript.sh +++ b/Containers/borgbackup/backupscript.sh @@ -24,22 +24,34 @@ for directory in "${VOLUME_DIRS[@]}"; do exit 1 fi done +# Test if default volumes are there +DEFAULT_VOLUMES=(nextcloud_aio_apache nextcloud_aio_nextcloud nextcloud_aio_database nextcloud_aio_database_dump nextcloud_aio_elasticsearch nextcloud_aio_nextcloud_data nextcloud_aio_mastercontainer) +for volume in "${DEFAULT_VOLUMES[@]}"; do + if ! mountpoint -q "/nextcloud_aio_volumes/$volume"; then + echo "$volume is missing which is not intended." + exit 1 + fi +done # Check if target is mountpoint -if ! mountpoint -q /mnt/borgbackup; then - echo "/mnt/borgbackup is not a mountpoint which is not allowed" +if [ -z "$BORG_REMOTE_REPO" ] && ! mountpoint -q "$MOUNT_DIR"; then + echo "$MOUNT_DIR is not a mountpoint which is not allowed." exit 1 fi -# Check if target is empty -if [ "$BORG_MODE" != backup ] && [ "$BORG_MODE" != test ] && ! [ -f "$BORG_BACKUP_DIRECTORY/config" ]; then - echo "The repository is empty. cannot perform check or restore." +# Check if repo is uninitialized +if [ "$BORG_MODE" != backup ] && [ "$BORG_MODE" != test ] && ! borg info > /dev/null; then + if [ -n "$BORG_REMOTE_REPO" ]; then + echo "The repository is uninitialized or cannot connect to remote. Cannot perform check or restore." + else + echo "The repository is uninitialized. Cannot perform check or restore." + fi exit 1 fi # Do not continue if this file exists (needed for simple external blocking) -if [ -f "$BORG_BACKUP_DIRECTORY/aio-lockfile" ]; then - echo "Not continuing because aio-lockfile exists - it seems like a script is externally running which is locking the backup archive." +if [ -z "$BORG_REMOTE_REPO" ] && [ -f "$BORG_BACKUP_DIRECTORY/aio-lockfile" ]; then + echo "Not continuing because aio-lockfile exists – it seems like a script is externally running which is locking the backup archive." echo "If this should not be the case, you can fix this by deleting the 'aio-lockfile' file from the backup archive directory." exit 1 fi @@ -49,6 +61,15 @@ if [ "$BORG_MODE" = backup ] || [ "$BORG_MODE" = restore ]; then touch "/nextcloud_aio_volumes/nextcloud_aio_database_dump/backup-is-running" fi +if [ -n "$BORG_REMOTE_REPO" ] && ! [ -f "$BORGBACKUP_KEY" ]; then + echo "First run, creating borg ssh key" + ssh-keygen -f "$BORGBACKUP_KEY" -N "" + echo "You should configure the remote to accept this public key" +fi +if [ -n "$BORG_REMOTE_REPO" ] && [ -f "$BORGBACKUP_KEY.pub" ]; then + echo "Your public ssh key for borgbackup is: $(cat "$BORGBACKUP_KEY.pub")" +fi + # Do the backup if [ "$BORG_MODE" = backup ]; then @@ -57,62 +78,97 @@ if [ "$BORG_MODE" = backup ]; then echo "configuration.json not present. Cannot perform the backup!" exit 1 elif ! [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud/config/config.php" ]; then - echo "config.php is missing cannot perform backup" + echo "config.php is missing. Cannot perform backup!" exit 1 elif ! [ -f "/nextcloud_aio_volumes/nextcloud_aio_database_dump/database-dump.sql" ]; then - echo "database-dump is missing. cannot perform backup" + echo "database-dump is missing. Cannot perform backup!" + echo "Please check the database container logs!" + exit 1 + elif ! [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/.ocdata" ] && ! [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/.ncdata" ]; then + echo "The .ncdata or .ocdata file is missing in Nextcloud datadir which means it is invalid!" + echo "Is the drive where the datadir is located on still mounted?" exit 1 fi - # Test that nothing is empty - for directory in "${VOLUME_DIRS[@]}"; do - if [ -z "$(ls -A "$directory")" ]; then - echo "$directory is empty which is not allowed." + # Test that default volumes are not empty + for volume in "${DEFAULT_VOLUMES[@]}"; do + if [ -z "$(ls -A "/nextcloud_aio_volumes/$volume")" ] && [ "$volume" != "nextcloud_aio_elasticsearch" ]; then + echo "/nextcloud_aio_volumes/$volume is empty which should not happen!" exit 1 fi done if [ -f "/nextcloud_aio_volumes/nextcloud_aio_database_dump/export.failed" ]; then - echo "Database export failed the last time. Most likely was the export time not high enough." echo "Cannot create a backup now." - echo "Please report this to https://github.com/nextcloud/all-in-one/issues. Thanks!" + echo "Reason is that the database export failed the last time." + echo "Most likely was the database container not correctly shut down via the AIO interface." + echo "" + echo "You might want to try the database export again manually by running the three commands:" + echo "sudo docker start nextcloud-aio-database" + echo "sleep 10" + echo "sudo docker stop nextcloud-aio-database -t 1800" + echo "" + echo "Afterwards try to create a backup again and it should hopefully work." + echo "If it should still fail, feel free to report this to https://github.com/nextcloud/all-in-one/issues and post the database container logs and the borgbackup container logs into the thread. Thanks!" exit 1 fi - # Create backup folder - mkdir -p "$BORG_BACKUP_DIRECTORY" + if [ -z "$BORG_REMOTE_REPO" ]; then + # Create backup folder + mkdir -p "$BORG_BACKUP_DIRECTORY" + fi - # Initialize the repository if the target is empty - if ! [ -f "$BORG_BACKUP_DIRECTORY/config" ]; then + # Initialize the repository if can't get info from target + if ! borg info > /dev/null; then # Don't initialize if already initialized if [ -f "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config" ]; then - echo "Cannot initialize a new repository as that was already done at least one time." + if [ -n "$BORG_REMOTE_REPO" ]; then + echo "Borg could not get info from the remote repo." + echo "This might be a failure to connect to the remote server. See the above borg info output for details." + else + echo "Borg could not get info from the targeted directory." + echo "This might happen if the targeted directory is located on an external drive and the drive not connected anymore. You should check this." + fi + echo "If you instead want to initialize a new backup repository, you may delete the 'borg.config' file that is stored in the mastercontainer volume manually, which will allow you to initialize a new borg repository in the chosen directory:" + echo "sudo docker exec nextcloud-aio-mastercontainer rm /mnt/docker-aio-config/data/borg.config" exit 1 fi - echo "initializing repository..." - if ! borg init --debug --encryption=repokey-blake2 "$BORG_BACKUP_DIRECTORY"; then + echo "Initializing repository..." + NEW_REPOSITORY=1 + if ! borg init --debug --encryption=repokey-blake2; then echo "Could not initialize borg repository." - rm -f "$BORG_BACKUP_DIRECTORY/config" exit 1 fi - borg config "$BORG_BACKUP_DIRECTORY" additional_free_space 2G - # Fix too large Borg cache - # https://borgbackup.readthedocs.io/en/stable/faq.html#the-borg-cache-eats-way-too-much-disk-space-what-can-i-do - BORG_ID="$(borg config "$BORG_BACKUP_DIRECTORY" id)" - rm -r "/root/.cache/borg/$BORG_ID/chunks.archive.d" - touch "/root/.cache/borg/$BORG_ID/chunks.archive.d" + if [ -z "$BORG_REMOTE_REPO" ]; then + # borg config only works for local repos; it's up to the remote to ensure the disk isn't full + borg config :: additional_free_space 2G - # Make a backup from the borg config file - if ! [ -f "$BORG_BACKUP_DIRECTORY/config" ]; then - echo "The borg config file wasn't created. Something is wrong." + # Fix too large Borg cache + # https://borgbackup.readthedocs.io/en/stable/faq.html#the-borg-cache-eats-way-too-much-disk-space-what-can-i-do + BORG_ID="$(borg config :: id)" + rm -r "/root/.cache/borg/$BORG_ID/chunks.archive.d" + touch "/root/.cache/borg/$BORG_ID/chunks.archive.d" + fi + + if ! borg info > /dev/null; then + echo "Borg can't get info from the repo it created. Something is wrong." exit 1 fi + rm -f "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config" - if ! cp "$BORG_BACKUP_DIRECTORY/config" "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config"; then - echo "Could not copy config file to second place. Cannot perform backup." - exit 1 + if [ -n "$BORG_REMOTE_REPO" ]; then + # `borg config` does not support remote repos so instead create a dummy file and rely on the remote to avoid + # corruption of the config file (which contains the encryption key). We don't actually use the contents of + # this file anywhere, so a touch is all we need so we remember we already initialized the repo. + touch "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config" + else + # Make a backup from the borg config file + if ! cp "$BORG_BACKUP_DIRECTORY/config" "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config"; then + echo "Could not copy config file to second place. Cannot perform backup." + exit 1 + fi fi echo "Repository successfully initialized." @@ -124,15 +180,47 @@ if [ "$BORG_MODE" = backup ]; then # Borg options # auto,zstd compression seems to has the best ratio based on: # https://forum.level1techs.com/t/optimal-compression-for-borg-backups/145870/6 - BORG_OPTS=(--stats --progress --compression "auto,zstd" --exclude-caches --checkpoint-interval 86400) + BORG_OPTS=(-v --stats --compression "auto,zstd") + if [ "$NEW_REPOSITORY" = 1 ]; then + BORG_OPTS+=(--progress) + fi + + # Exclude the nextcloud log and audit log for GDPR reasons + BORG_EXCLUDE=(--exclude "/nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/nextcloud.log*" --exclude "/nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/audit.log" --exclude "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/lost+found") + BORG_INCLUDE=() + + # Exclude datadir if .noaiobackup file was found + # shellcheck disable=SC2144 + if [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/.noaiobackup" ]; then + BORG_EXCLUDE+=(--exclude "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/") + BORG_INCLUDE+=(--pattern="+/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/.noaiobackup") + echo "⚠️⚠️⚠️ '.noaiobackup' file was found in Nextcloud's data directory. Excluding the data directory from backup!" + # Exclude preview folder if .noaiobackup file was found + elif [ -f /nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_*/preview/.noaiobackup ]; then + BORG_EXCLUDE+=(--exclude "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_*/preview/") + BORG_INCLUDE+=(--pattern="+/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_*/preview/.noaiobackup") + echo "⚠️⚠️⚠️ '.noaiobackup' file was found in the preview directory. Excluding the preview directory from backup!" + fi + + # Make sure that there is always a borg.config file before creating a new backup + if ! [ -f "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config" ]; then + echo "Did not find borg.config file in the mastercontainer volume." + echo "Cannot create a backup as this is wrong." + exit 1 + fi # Create the backup echo "Starting the backup..." get_start_time - if ! borg create "${BORG_OPTS[@]}" "$BORG_BACKUP_DIRECTORY::$CURRENT_DATE-nextcloud-aio" "/nextcloud_aio_volumes/"; then + if ! borg create "${BORG_OPTS[@]}" "${BORG_INCLUDE[@]}" "${BORG_EXCLUDE[@]}" "::$CURRENT_DATE-nextcloud-aio" "/nextcloud_aio_volumes/" --exclude-from /borg_excludes; then echo "Deleting the failed backup archive..." - borg delete --stats --progress "$BORG_BACKUP_DIRECTORY::$CURRENT_DATE-nextcloud-aio" + borg delete --stats "::$CURRENT_DATE-nextcloud-aio" echo "Backup failed!" + echo "You might want to check the backup integrity via the AIO interface." + if [ "$NEW_REPOSITORY" = 1 ]; then + echo "Deleting borg.config file so that you can choose a different location for the backup." + rm "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config" + fi exit 1 fi @@ -140,15 +228,23 @@ if [ "$BORG_MODE" = backup ]; then rm -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/skip.update" # Prune options - BORG_PRUNE_OPTS=(--stats --progress --keep-within=7d --keep-weekly=4 --keep-monthly=6 "$BORG_BACKUP_DIRECTORY") + read -ra BORG_PRUNE_OPTS <<< "$BORG_RETENTION_POLICY" + echo "BORG_PRUNE_OPTS are ${BORG_PRUNE_OPTS[*]}" # Prune archives echo "Pruning the archives..." - if ! borg prune --prefix '*_*-nextcloud-aio' "${BORG_PRUNE_OPTS[@]}"; then + if ! borg prune --stats --glob-archives '*_*-nextcloud-aio' "${BORG_PRUNE_OPTS[@]}"; then echo "Failed to prune archives!" exit 1 fi + # Compact archives + echo "Compacting the archives..." + if ! borg compact; then + echo "Failed to compact archives!" + exit 1 + fi + # Back up additional directories of the host if [ "$ADDITIONAL_DIRECTORIES_BACKUP" = 'yes' ]; then if [ -d "/docker_volumes/" ]; then @@ -160,17 +256,23 @@ if [ "$BORG_MODE" = backup ]; then exit 1 fi done - if ! borg create "${BORG_OPTS[@]}" "$BORG_BACKUP_DIRECTORY::$CURRENT_DATE-additional-docker-volumes" "/docker_volumes/"; then + echo "Starting the backup for additional volumes..." + if ! borg create "${BORG_OPTS[@]}" "::$CURRENT_DATE-additional-docker-volumes" "/docker_volumes/"; then echo "Deleting the failed backup archive..." - borg delete --stats --progress "$BORG_BACKUP_DIRECTORY::$CURRENT_DATE-additional-docker-volumes" + borg delete --stats "::$CURRENT_DATE-additional-docker-volumes" echo "Backup of additional docker-volumes failed!" exit 1 fi - - if ! borg prune --prefix '*_*-additional-docker-volumes' "${BORG_PRUNE_OPTS[@]}"; then + echo "Pruning additional volumes..." + if ! borg prune --stats --glob-archives '*_*-additional-docker-volumes' "${BORG_PRUNE_OPTS[@]}"; then echo "Failed to prune additional docker-volumes archives!" exit 1 fi + echo "Compacting additional volumes..." + if ! borg compact; then + echo "Failed to compact additional docker-volume archives!" + exit 1 + fi fi if [ -d "/host_mounts/" ]; then EXCLUDED_DIRECTORIES=(home/*/.cache root/.cache var/cache lost+found run var/run dev tmp sys proc) @@ -184,22 +286,29 @@ if [ "$BORG_MODE" = backup ]; then do EXCLUDE_DIRS+=(--exclude "/host_mounts/$directory/") done - if ! borg create "${BORG_OPTS[@]}" "${EXCLUDE_DIRS[@]}" "$BORG_BACKUP_DIRECTORY::$CURRENT_DATE-additional-host-mounts" "/host_mounts/"; then + echo "Starting the backup for additional host mounts..." + if ! borg create "${BORG_OPTS[@]}" "${EXCLUDE_DIRS[@]}" "::$CURRENT_DATE-additional-host-mounts" "/host_mounts/"; then echo "Deleting the failed backup archive..." - borg delete --stats --progress "$BORG_BACKUP_DIRECTORY::$CURRENT_DATE-additional-host-mounts" + borg delete --stats "::$CURRENT_DATE-additional-host-mounts" echo "Backup of additional host-mounts failed!" exit 1 fi - if ! borg prune --prefix '*_*-additional-host-mounts' "${BORG_PRUNE_OPTS[@]}"; then + echo "Pruning additional host mounts..." + if ! borg prune --stats --glob-archives '*_*-additional-host-mounts' "${BORG_PRUNE_OPTS[@]}"; then echo "Failed to prune additional host-mount archives!" exit 1 fi + echo "Compacting additional host mounts..." + if ! borg compact; then + echo "Failed to compact additional host-mount archives!" + exit 1 + fi fi fi # Inform user get_expiration_time - echo "Backup finished successfully on $END_DATE_READABLE ($DURATION_READABLE)" + echo "Backup finished successfully on $END_DATE_READABLE ($DURATION_READABLE)." if [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/update.failed" ]; then echo "However a Nextcloud update failed. So reporting that the backup failed which will skip any update attempt the next time." echo "Please restore a backup from before the failed Nextcloud update attempt." @@ -212,17 +321,38 @@ fi if [ "$BORG_MODE" = restore ]; then get_start_time - # Perform the restore + # Pick archive to restore if [ -n "$SELECTED_RESTORE_TIME" ]; then - SELECTED_ARCHIVE="$(borg list "$BORG_BACKUP_DIRECTORY" | grep "nextcloud-aio" | grep "$SELECTED_RESTORE_TIME" | awk -F " " '{print $1}' | head -1)" + SELECTED_ARCHIVE="$(borg list | grep "nextcloud-aio" | grep "$SELECTED_RESTORE_TIME" | awk -F " " '{print $1}' | head -1)" else - SELECTED_ARCHIVE="$(borg list "$BORG_BACKUP_DIRECTORY" | grep "nextcloud-aio" | awk -F " " '{print $1}' | sort -r | head -1)" + SELECTED_ARCHIVE="$(borg list | grep "nextcloud-aio" | awk -F " " '{print $1}' | sort -r | head -1)" fi echo "Restoring '$SELECTED_ARCHIVE'..." - mkdir -p /tmp/borg - if ! borg mount "$BORG_BACKUP_DIRECTORY::$SELECTED_ARCHIVE" /tmp/borg; then - echo "Could not mount the backup!" - exit 1 + + ADDITIONAL_RSYNC_EXCLUDES=() + ADDITIONAL_BORG_EXCLUDES=() + ADDITIONAL_FIND_EXCLUDES=() + # Exclude datadir if .noaiobackup file was found + # shellcheck disable=SC2144 + if [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/.noaiobackup" ]; then + # Keep these 3 in sync. Beware, the pattern syntax and the paths differ + ADDITIONAL_RSYNC_EXCLUDES=(--exclude "nextcloud_aio_nextcloud_data/**") + ADDITIONAL_BORG_EXCLUDES=(--exclude "sh:nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/**") + ADDITIONAL_FIND_EXCLUDES=(-o -regex 'nextcloud_aio_volumes/nextcloud_aio_nextcloud_data\(/.*\)?') + echo "⚠️⚠️⚠️ '.noaiobackup' file was found in Nextcloud's data directory. Excluding the data directory from restore!" + echo "You might run into problems due to this afterwards as potentially this makes the directory go out of sync with the database." + echo "You might be able to fix this by running 'occ files:scan --all' and 'occ maintenance:repair' and 'occ files:scan-app-data' after the restore." + echo "See https://github.com/nextcloud/all-in-one#how-to-run-occ-commands" + # Exclude previews from restore if selected to speed up process or exclude preview folder if .noaiobackup file was found + elif [ -n "$RESTORE_EXCLUDE_PREVIEWS" ] || [ -f /nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_*/preview/.noaiobackup ]; then + # Keep these 3 in sync. Beware, the pattern syntax and the paths differ + ADDITIONAL_RSYNC_EXCLUDES=(--exclude "nextcloud_aio_nextcloud_data/appdata_*/preview/**") + ADDITIONAL_BORG_EXCLUDES=(--exclude "sh:nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_*/preview/**") + ADDITIONAL_FIND_EXCLUDES=(-o -regex 'nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_[^/]*/preview\(/.*\)?') + echo "⚠️⚠️⚠️ Excluding previews from restore!" + echo "You might run into problems due to this afterwards as potentially this makes the directory go out of sync with the database." + echo "You might be able to fix this by running 'occ files:scan-app-data preview' after the restore." + echo "See https://github.com/nextcloud/all-in-one#how-to-run-occ-commands" fi # Save Additional Backup dirs @@ -235,23 +365,12 @@ if [ "$BORG_MODE" = restore ]; then DAILY_BACKUPTIME="$(cat /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_time)" fi - # Restore everything except the configuration file - if ! rsync --stats --archive --human-readable -vv --delete \ - --exclude "nextcloud_aio_mastercontainer/session/"** \ - --exclude "nextcloud_aio_mastercontainer/certs/"** \ - --exclude "nextcloud_aio_mastercontainer/data/daily_backup_running" \ - --exclude "nextcloud_aio_mastercontainer/data/configuration.json" \ - /tmp/borg/nextcloud_aio_volumes/ /nextcloud_aio_volumes; then - echo "Something failed while restoring from backup." - umount /tmp/borg - exit 1 - fi - # Save current aio password AIO_PASSWORD="$(jq '.password' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" - # Save current path + # Save current backup location vars BORG_LOCATION="$(jq '.borg_backup_host_location' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + REMOTE_REPO="$(jq '.borg_remote_repo' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" # Save current nextcloud datadir if grep -q '"nextcloud_datadir":' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json; then @@ -260,22 +379,116 @@ if [ "$BORG_MODE" = restore ]; then NEXTCLOUD_DATADIR='""' fi - # Restore the configuration file - if ! rsync --archive --human-readable -vv \ - /tmp/borg/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json \ - /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json; then - echo "Something failed while restoring the configuration.json." - umount /tmp/borg - exit 1 + if [ -z "$BORG_REMOTE_REPO" ]; then + mkdir -p /tmp/borg + if ! borg mount "::$SELECTED_ARCHIVE" /tmp/borg; then + echo "Could not mount the backup!" + exit 1 + fi + + # Restore everything except the configuration file + # + # These exclude patterns need to be kept in sync with the borg_excludes file and the find excludes in this file, + # which use a different syntax (patterns appear in 3 places in total) + if ! rsync --stats --archive --human-readable -vv --delete \ + --exclude "nextcloud_aio_apache/caddy/**" \ + --exclude "nextcloud_aio_mastercontainer/caddy/**" \ + --exclude "nextcloud_aio_nextcloud/data/nextcloud.log*" \ + --exclude "nextcloud_aio_nextcloud/data/audit.log" \ + --exclude "nextcloud_aio_mastercontainer/certs/**" \ + --exclude "nextcloud_aio_mastercontainer/data/configuration.json" \ + --exclude "nextcloud_aio_mastercontainer/data/daily_backup_running" \ + --exclude "nextcloud_aio_mastercontainer/data/session_date_file" \ + --exclude "nextcloud_aio_mastercontainer/session/**" \ + --exclude "nextcloud_aio_nextcloud_data/lost+found" \ + "${ADDITIONAL_RSYNC_EXCLUDES[@]}" \ + /tmp/borg/nextcloud_aio_volumes/ /nextcloud_aio_volumes/; then + RESTORE_FAILED=1 + echo "Something failed while restoring from backup." + fi + + # Restore the configuration file + if ! rsync --archive --human-readable -vv \ + /tmp/borg/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json \ + /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json; then + RESTORE_FAILED=1 + echo "Something failed while restoring the configuration.json." + fi + + if ! umount /tmp/borg; then + echo "Failed to unmount the borg archive but should still be able to restore successfully" + fi + else + # Restore nearly everything + # + # borg mount is really slow for remote repos (did not check whether it's slow for local repos too), + # using extract to /tmp would require temporarily storing a second copy of the data. + # So instead extract directly on top of the destination with exclude patterns for the config, but + # then we do still need to delete local files which are not present in the archive. + # + # Older backups may still contain files we've since excluded, so we have to exclude on extract as well. + cd / # borg extract has no destination arg and extracts to CWD + if ! borg extract "::$SELECTED_ARCHIVE" --progress --exclude-from /borg_excludes "${ADDITIONAL_BORG_EXCLUDES[@]}" --pattern '+nextcloud_aio_volumes/**' + then + RESTORE_FAILED=1 + echo "Failed to extract backup archive." + else + # Delete files/dirs present locally, but not in the backup archive, excluding conf files + # https://unix.stackexchange.com/a/759341 + # This comm does not support -z, but I doubt any file names would have \n in them + # + # These find patterns need to be kept in sync with the borg_excludes file and the rsync excludes in this + # file, which use a different syntax (patterns appear in 3 places in total) + echo "Deleting local files which do not exist in the backup" + if ! find nextcloud_aio_volumes \ + -not \( \ + -path nextcloud_aio_volumes/nextcloud_aio_apache/caddy \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_apache/caddy/*" \ + -o -path nextcloud_aio_volumes/nextcloud_aio_mastercontainer/caddy \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_mastercontainer/caddy/*" \ + -o -path nextcloud_aio_volumes/nextcloud_aio_mastercontainer/certs \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_mastercontainer/certs/*" \ + -o -path nextcloud_aio_volumes/nextcloud_aio_mastercontainer/session \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_mastercontainer/session/*" \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/nextcloud.log*" \ + -o -path nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/audit.log \ + -o -path nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_running \ + -o -path nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/session_date_file \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/id_borg*" \ + -o -path "nextcloud_aio_nextcloud_data/lost+found" \ + "${ADDITIONAL_FIND_EXCLUDES[@]}" \ + \) \ + | LC_ALL=C sort \ + | LC_ALL=C comm -23 - \ + <(borg list "::$SELECTED_ARCHIVE" --short --exclude-from /borg_excludes --pattern '+nextcloud_aio_volumes/**' | LC_ALL=C sort) \ + > /tmp/local_files_not_in_backup + then + RESTORE_FAILED=1 + echo "Failed to delete local files not in backup archive." + else + # More robust than e.g. xargs as I got a ~"args line too long" error while testing that, but it's slower + # https://stackoverflow.com/a/21848934 + while IFS= read -r file + do rm -vrf -- "$file" || DELETE_FAILED=1 + done < /tmp/local_files_not_in_backup + + if [ "$DELETE_FAILED" = 1 ]; then + RESTORE_FAILED=1 + echo "Failed to delete (some) local files not in backup archive." + fi + fi + fi fi # Set backup-mode to restore since it was a restore CONTENTS="$(jq '."backup-mode" = "restore"' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" echo -E "${CONTENTS}" > /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json - # Reset the backup path to the currently used one + # Reset the backup location vars to the currently used one CONTENTS="$(jq ".borg_backup_host_location = $BORG_LOCATION" /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" echo -E "${CONTENTS}" > /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json + CONTENTS="$(jq ".borg_remote_repo = $REMOTE_REPO" /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + echo -E "${CONTENTS}" > /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json # Reset the AIO password to the currently used one CONTENTS="$(jq ".password = $AIO_PASSWORD" /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" @@ -299,11 +512,13 @@ if [ "$BORG_MODE" = restore ]; then chmod 770 "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_time" fi - umount /tmp/borg + if [ "$RESTORE_FAILED" = 1 ]; then + exit 1 + fi # Inform user get_expiration_time - echo "Restore finished successfully on $END_DATE_READABLE ($DURATION_READABLE)" + echo "Restore finished successfully on $END_DATE_READABLE ($DURATION_READABLE)." # Add file to Nextcloud container so that it skips any update the next time touch "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/skip.update" @@ -312,6 +527,15 @@ if [ "$BORG_MODE" = restore ]; then # Add file to Nextcloud container so that it performs a fingerprint update the next time touch "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/fingerprint.update" chmod 777 "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/fingerprint.update" + + # Add file to Netcloud container to trigger a preview scan the next time it starts + if [ -n "$RESTORE_EXCLUDE_PREVIEWS" ]; then + touch "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/trigger-preview.scan" + chmod 777 "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/trigger-preview.scan" + fi + + # Delete redis cache + rm -f "/mnt/redis/dump.rdb" fi # Do the Backup check @@ -320,37 +544,80 @@ if [ "$BORG_MODE" = check ]; then echo "Checking the backup integrity..." # Perform the check - if ! borg check --verify-data --progress "$BORG_BACKUP_DIRECTORY"; then + if ! borg check -v --verify-data; then echo "Some errors were found while checking the backup integrity!" + echo "Check the AIO interface for advice on how to proceed now!" exit 1 fi # Inform user get_expiration_time - echo "Check finished successfully on $END_DATE_READABLE ($DURATION_READABLE)" + echo "Check finished successfully on $END_DATE_READABLE ($DURATION_READABLE)." + exit 0 +fi + +# Do the Backup check-repair +if [ "$BORG_MODE" = "check-repair" ]; then + get_start_time + echo "Checking the backup integrity and repairing it..." + + # Perform the check-repair + if ! echo YES | borg check -v --repair; then + echo "Some errors were found while checking and repairing the backup integrity!" + exit 1 + fi + + # Inform user + get_expiration_time + echo "Check finished successfully on $END_DATE_READABLE ($DURATION_READABLE)." exit 0 fi # Do the backup test if [ "$BORG_MODE" = test ]; then - if ! [ -d "$BORG_BACKUP_DIRECTORY" ]; then - echo "No 'borg' directory in the given backup directory found!" - echo "Only the files/folders below have been found in the given directory." - ls -a "$MOUNT_DIR" - echo "Please adjust the directory so that the borg archive is positioned in a folder named 'borg' inside the given directory!" - exit 1 - elif ! [ -f "$BORG_BACKUP_DIRECTORY/config" ]; then - echo "A 'borg' directory was found but could not find the borg archive." - echo "Only the files/folders below have been found in the borg directory." - ls -a "$BORG_BACKUP_DIRECTORY" - echo "The archive and most importantly the config file must be positioned directly in the 'borg' subfolder." - exit 1 - elif ! borg list "$BORG_BACKUP_DIRECTORY"; then + if [ -n "$BORG_REMOTE_REPO" ]; then + if ! borg info > /dev/null; then + echo "Borg could not get info from the remote repo." + echo "See the above borg info output for details." + exit 1 + fi + else + if ! [ -d "$BORG_BACKUP_DIRECTORY" ]; then + echo "No 'borg' directory in the given backup directory found!" + echo "Only the files/folders below have been found in the given directory." + ls -a "$MOUNT_DIR" + echo "Please adjust the directory so that the borg archive is positioned in a folder named 'borg' inside the given directory!" + exit 1 + elif ! [ -f "$BORG_BACKUP_DIRECTORY/config" ]; then + echo "A 'borg' directory was found but could not find the borg archive." + echo "Only the files/folders below have been found in the borg directory." + ls -a "$BORG_BACKUP_DIRECTORY" + echo "The archive and most importantly the config file must be positioned directly in the 'borg' subfolder." + exit 1 + fi + fi + + if ! borg list >/dev/null; then echo "The entered path seems to be valid but could not open the backup archive." echo "Most likely the entered password was wrong so please adjust it accordingly!" exit 1 else - echo "Everything looks fine so feel free to continue!" - exit 0 + if ! borg list | grep "nextcloud-aio"; then + echo "The backup archive does not contain a valid Nextcloud AIO backup." + echo "Most likely was the archive not created via Nextcloud AIO." + exit 1 + else + echo "Everything looks fine so feel free to continue!" + exit 0 + fi fi fi + +if [ "$BORG_MODE" = list ]; then + echo "Updating backup list..." + if ! borg info > /dev/null; then + echo "Could not update the backup list." + exit 1 + fi + # The update gets done automatically in the wrapper start.sh script. +fi diff --git a/Containers/borgbackup/borg_excludes b/Containers/borgbackup/borg_excludes new file mode 100644 index 00000000..bbe6adaa --- /dev/null +++ b/Containers/borgbackup/borg_excludes @@ -0,0 +1,11 @@ +# These patterns need to be kept in sync with rsync and find excludes in backupscript.sh, +# which use a different syntax (patterns appear in 3 places in total) +nextcloud_aio_volumes/nextcloud_aio_apache/caddy/ +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/caddy/ +nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/nextcloud.log* +nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/audit.log +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/certs/ +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_running +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/session_date_file +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/session/ +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/id_borg* \ No newline at end of file diff --git a/Containers/borgbackup/start.sh b/Containers/borgbackup/start.sh index 00e06ed4..bb7a8a6a 100644 --- a/Containers/borgbackup/start.sh +++ b/Containers/borgbackup/start.sh @@ -2,7 +2,7 @@ # Variables export MOUNT_DIR="/mnt/borgbackup" -export BORG_BACKUP_DIRECTORY="$MOUNT_DIR/borg" +export BORG_BACKUP_DIRECTORY="$MOUNT_DIR/borg" # necessary even when remote to store the aio-lockfile # Validate BORG_PASSWORD if [ -z "$BORG_PASSWORD" ] && [ -z "$BACKUP_RESTORE_PASSWORD" ]; then @@ -18,10 +18,22 @@ else fi export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes +if [ -n "$BORG_REMOTE_REPO" ]; then + export BORG_REPO="$BORG_REMOTE_REPO" + + # Location to create the borg ssh pub/priv key + export BORGBACKUP_KEY="/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/id_borg" + + # Accept any host key the first time connecting to the remote. Strictly speaking should be provided by user but you'd + # have to be very unlucky to get MitM'ed on your first connection. + export BORG_RSH="ssh -o StrictHostKeyChecking=accept-new -i $BORGBACKUP_KEY" +else + export BORG_REPO="$BORG_BACKUP_DIRECTORY" +fi # Validate BORG_MODE -if [ "$BORG_MODE" != backup ] && [ "$BORG_MODE" != restore ] && [ "$BORG_MODE" != check ] && [ "$BORG_MODE" != test ]; then - echo "No correct BORG_MODE mode applied. Valid are 'backup', 'check', 'restore' and 'test'." +if [ "$BORG_MODE" != backup ] && [ "$BORG_MODE" != restore ] && [ "$BORG_MODE" != check ] && [ "$BORG_MODE" != "check-repair" ] && [ "$BORG_MODE" != "test" ] && [ "$BORG_MODE" != "list" ]; then + echo "No correct BORG_MODE mode applied. Valid are 'backup', 'check', 'restore', 'test' and 'list'." exit 1 fi @@ -36,8 +48,8 @@ fi rm -f "/nextcloud_aio_volumes/nextcloud_aio_database_dump/backup-is-running" # Get a list of all available borg archives -if borg list "$BORG_BACKUP_DIRECTORY" &>/dev/null; then - borg list "$BORG_BACKUP_DIRECTORY" | grep "nextcloud-aio" | awk -F " " '{print $1","$3,$4}' > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list" +if borg list &>/dev/null; then + borg list | grep "nextcloud-aio" | awk -F " " '{print $1","$3,$4}' > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list" else echo "" > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list" fi diff --git a/Containers/clamav/Dockerfile b/Containers/clamav/Dockerfile index 98fc3fef..6910ae1c 100644 --- a/Containers/clamav/Dockerfile +++ b/Containers/clamav/Dockerfile @@ -1,6 +1,38 @@ -# Probably from this file: https://github.com/Cisco-Talos/clamav/blob/main/Dockerfile -FROM clamav/clamav:0.105.1 +# syntax=docker/dockerfile:latest +FROM alpine:3.23.3 -RUN apk add --update --no-cache tzdata -COPY clamav.conf /tmp/ -RUN cat /tmp/clamav.conf >> /etc/clamav/clamd.conf +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache tzdata clamav clamav-milter supervisor bash; \ + mkdir -p /tmp /var/lib/clamav /run/clamav /var/log/supervisord /var/run/supervisord; \ + chmod 777 -R /tmp /run/clamav /var/log/clamav /var/log/supervisord /var/run/supervisord; \ + chown -R 100:100 /var/lib/clamav; \ + sed -i "s|#\?MaxDirectoryRecursion.*|MaxDirectoryRecursion 30|g" /etc/clamav/clamd.conf; \ + sed -i "s|#\?MaxScanSize.*|MaxScanSize 2000M|g" /etc/clamav/clamd.conf; \ + sed -i "s|#\?MaxFileSize.*|MaxFileSize 2000M|g" /etc/clamav/clamd.conf; \ + sed -i "s|#\?PCREMaxFileSize.*|PCREMaxFileSize 2000M|g" /etc/clamav/clamd.conf; \ +# StreamMaxLength must be synced with av_stream_max_length inside the Nextcloud files_antivirus plugin + sed -i "s|#\?StreamMaxLength.*|StreamMaxLength 2000M|g" /etc/clamav/clamd.conf; \ + sed -i "s|#\?TCPSocket|TCPSocket|g" /etc/clamav/clamd.conf; \ + sed -i "s|^LocalSocket .*|LocalSocket /tmp/clamd.sock|g" /etc/clamav/clamd.conf; \ + sed -i "s|Example| |g" /etc/clamav/clamav-milter.conf; \ + sed -i "s|#\?MilterSocket inet:7357|MilterSocket inet:7357|g" /etc/clamav/clamav-milter.conf; \ + sed -i "s|#\?ClamdSocket unix:/run/clamav/clamd.sock|ClamdSocket unix:/tmp/clamd.sock|g" /etc/clamav/clamav-milter.conf; \ + sed -i "s|#\?OnInfected Quarantine|OnInfected Reject|g" /etc/clamav/clamav-milter.conf; \ + sed -i "s|#\?AddHeader Replace|AddHeader Add|g" /etc/clamav/clamav-milter.conf; \ + sed -i "s|#\?Foreground yes|Foreground yes|g" /etc/clamav/clamav-milter.conf + +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh +COPY --chmod=664 supervisord.conf /supervisord.conf + +USER 100 +RUN set -ex; \ + freshclam --foreground --stdout +VOLUME /var/lib/clamav +ENTRYPOINT ["/start.sh"] +CMD ["/usr/bin/supervisord", "-c", "/supervisord.conf"] +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" +HEALTHCHECK --start-period=60s --retries=9 CMD /healthcheck.sh diff --git a/Containers/clamav/clamav.conf b/Containers/clamav/clamav.conf deleted file mode 100644 index 1de35086..00000000 --- a/Containers/clamav/clamav.conf +++ /dev/null @@ -1,4 +0,0 @@ -MaxDirectoryRecursion 30 -MaxFileSize 100M -PCREMaxFileSize 100M -StreamMaxLength 100M diff --git a/Containers/clamav/healthcheck.sh b/Containers/clamav/healthcheck.sh new file mode 100644 index 00000000..fe8b5daa --- /dev/null +++ b/Containers/clamav/healthcheck.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if [ "$(echo "PING" | nc 127.0.0.1 3310)" != "PONG" ]; then + echo "ERROR: Unable to contact server" + exit 1 +fi + +echo "Clamd is up" +exit 0 diff --git a/Containers/clamav/start.sh b/Containers/clamav/start.sh new file mode 100644 index 00000000..609120c1 --- /dev/null +++ b/Containers/clamav/start.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Print out clamav version for compliance reasons +clamscan --version + +echo "Clamav started" + +exec "$@" diff --git a/Containers/clamav/supervisord.conf b/Containers/clamav/supervisord.conf new file mode 100644 index 00000000..1895ceb6 --- /dev/null +++ b/Containers/clamav/supervisord.conf @@ -0,0 +1,30 @@ +[supervisord] +nodaemon=true +nodaemon=true +logfile=/var/log/supervisord/supervisord.log +pidfile=/var/run/supervisord/supervisord.pid +childlogdir=/var/log/supervisord/ +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=error + +[program:freshclam] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=freshclam --foreground --stdout --daemon --daemon-notify=/etc/clamav/clamd.conf + +[program:clamd] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=clamd --foreground --config-file=/etc/clamav/clamd.conf + +[program:milter] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=clamav-milter --config-file=/etc/clamav/clamav-milter.conf \ No newline at end of file diff --git a/Containers/collabora-online/Dockerfile b/Containers/collabora-online/Dockerfile new file mode 100644 index 00000000..ec8b63f0 --- /dev/null +++ b/Containers/collabora-online/Dockerfile @@ -0,0 +1,16 @@ +# syntax=docker/dockerfile:latest +# From https://gitlab.collabora.com/collabora-online/docker +# hadolint ignore=DL3007 +FROM registry.gitlab.collabora.com/collabora-online/docker:latest + +USER root +ARG DEBIAN_FRONTEND=noninteractive + +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +USER 1001 + +HEALTHCHECK --start-period=60s --retries=9 CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/collabora-online/healthcheck.sh b/Containers/collabora-online/healthcheck.sh new file mode 100644 index 00000000..45e9278b --- /dev/null +++ b/Containers/collabora-online/healthcheck.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Unfortunately, no curl and no nc is installed in the container +# and packages can also not be added as the package list is broken. +# So always exiting 0 for now. +# nc http://127.0.0.1:9980 || exit 1 +exit 0 diff --git a/Containers/collabora/Dockerfile b/Containers/collabora/Dockerfile index 0873e2dc..d1693da0 100644 --- a/Containers/collabora/Dockerfile +++ b/Containers/collabora/Dockerfile @@ -1,17 +1,15 @@ -# From a file located probably somewhere here: https://github.com/CollaboraOnline/online/tree/master/docker -FROM collabora/code:22.05.5.4.1 +# syntax=docker/dockerfile:latest +# From a file located probably somewhere here: https://github.com/CollaboraOnline/online/blob/master/docker/from-packages/Dockerfile +FROM collabora/code:25.04.8.2.1 USER root +ARG DEBIAN_FRONTEND=noninteractive -RUN set -ex; \ - \ - apt-get update; \ - export DEBIAN_FRONTEND=noninteractive; \ - apt-get install -y --no-install-recommends \ - tzdata \ - ; \ - rm -rf /var/lib/apt/lists/* +COPY --chmod=775 healthcheck.sh /healthcheck.sh -USER 104 +USER 1001 -HEALTHCHECK CMD curl -skfI localhost:9980 || exit 1 +HEALTHCHECK --start-period=60s --retries=9 CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/collabora/healthcheck.sh b/Containers/collabora/healthcheck.sh new file mode 100644 index 00000000..45e9278b --- /dev/null +++ b/Containers/collabora/healthcheck.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Unfortunately, no curl and no nc is installed in the container +# and packages can also not be added as the package list is broken. +# So always exiting 0 for now. +# nc http://127.0.0.1:9980 || exit 1 +exit 0 diff --git a/Containers/docker-socket-proxy/Dockerfile b/Containers/docker-socket-proxy/Dockerfile new file mode 100644 index 00000000..ffc867a8 --- /dev/null +++ b/Containers/docker-socket-proxy/Dockerfile @@ -0,0 +1,23 @@ +# syntax=docker/dockerfile:latest +FROM haproxy:3.3.2-alpine + +# hadolint ignore=DL3002 +USER root +ENV NEXTCLOUD_HOST=nextcloud-aio-nextcloud +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + ca-certificates \ + tzdata \ + bash \ + bind-tools; \ + chmod -R 777 /tmp + +COPY --chmod=775 *.sh / +COPY --chmod=664 haproxy.cfg /haproxy.cfg + +ENTRYPOINT ["/start.sh"] +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/docker-socket-proxy/haproxy.cfg b/Containers/docker-socket-proxy/haproxy.cfg new file mode 100644 index 00000000..632df434 --- /dev/null +++ b/Containers/docker-socket-proxy/haproxy.cfg @@ -0,0 +1,68 @@ +# Inspiration: https://github.com/Tecnativa/docker-socket-proxy/blob/master/haproxy.cfg + +global + maxconn 10 + +defaults + timeout connect 30s + timeout client 30s + timeout server 1800s + +frontend http + mode http + bind :::2375 v4v6 + http-request deny unless { src 127.0.0.1 } || { src ::1 } || { src NC_IPV4_PLACEHOLDER } || { src NC_IPV6_PLACEHOLDER } + # docker system _ping + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping$ } METH_GET + # docker inspect image: GET images/%s/json + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/images/.*/json } METH_GET + # container inspect: GET containers/%s/json + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+/json } METH_GET + # container inspect: GET containers/%s/logs + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+/logs } METH_GET + # container start/stop: POST containers/%s/start containers/%s/stop + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+/((start)|(stop)) } METH_POST + # container rm: DELETE containers/%s + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+ } METH_DELETE + # container update/exec: POST containers/%s/update containers/%s/exec + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+/((update)|(exec)) } METH_POST + # container put: PUT containers/%s/archive + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+/archive } METH_PUT + # run exec instance: POST exec/%s + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/exec/[a-zA-Z0-9_.-]+/start } METH_POST + + # container create: POST containers/create?name=%s + # ACL to restrict container name to nc_app_[a-zA-Z0-9_.-]+ + acl nc_app_container_name url_param(name) -m reg -i "^nc_app_[a-zA-Z0-9_.-]+" + + # ACL to restrict the number of Mounts to 1 + acl one_mount_volume req.body -m reg -i "\"Mounts\"\s*:\s*\[\s*(?:(?!\"Mounts\"\s*:\s*\[)[^}]*)}[^}]*\]" + # ACL to deny if there are any binds + acl binds_present req.body -m reg -i "\"HostConfig\"\s*:.*\"Binds\"\s*:" + # ACL to restrict the type of Mounts to volume + acl type_not_volume req.body -m reg -i "\"Mounts\"\s*:\s*\[[^\]]*(\"Type\"\s*:\s*\"(?!volume\b)\w+\"[^\]]*)+\]" + http-request deny if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/create } nc_app_container_name !one_mount_volume binds_present type_not_volume METH_POST + + # ACL to restrict container creation, that it has HostConfig.Privileged(by searching for "Privileged" word in all payload) + acl no_privileged_flag req.body -m reg -i "\"Privileged\"" + # ACL to allow mount volume with strict pattern for name: nc_app_[a-zA-Z0-9_.-]+_data + acl nc_app_volume_data_only req.body -m reg -i "\"Mounts\"\s*:\s*\[\s*{[^}]*\"Source\"\s*:\s*\"nc_app_[a-zA-Z0-9_.-]+_data\"" + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/create } nc_app_container_name !no_privileged_flag nc_app_volume_data_only METH_POST + # end of container create + + # volume create: POST volumes/create + # restrict name + acl nc_app_volume_data req.body -m reg -i "\"Name\"\s*:\s*\"nc_app_[a-zA-Z0-9_.-]+_data\"" + # do not allow to use "device" word e.g., "--opt device=:/path/to/dir" + acl volume_no_device req.body -m reg -i "\"device\"" + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/volumes/create } nc_app_volume_data !volume_no_device METH_POST + # volume rm: DELETE volumes/%s + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/volumes/nc_app_[a-zA-Z0-9_.-]+_data } METH_DELETE + # image pull: POST images/create?fromImage=%s + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/images/create } METH_POST + http-request deny + default_backend dockerbackend + +backend dockerbackend + mode http + server dockersocket /var/run/docker.sock diff --git a/Containers/docker-socket-proxy/healthcheck.sh b/Containers/docker-socket-proxy/healthcheck.sh new file mode 100644 index 00000000..d89deb6b --- /dev/null +++ b/Containers/docker-socket-proxy/healthcheck.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +nc -z "$NEXTCLOUD_HOST" 9001 || exit 0 +nc -z 127.0.0.1 2375 || exit 1 diff --git a/Containers/docker-socket-proxy/start.sh b/Containers/docker-socket-proxy/start.sh new file mode 100644 index 00000000..657c914e --- /dev/null +++ b/Containers/docker-socket-proxy/start.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# Only start container if nextcloud is accessible +while ! nc -z "$NEXTCLOUD_HOST" 9001; do + echo "Waiting for Nextcloud to start..." + sleep 5 +done + +set -x +IPv4_ADDRESS_NC="$(dig nextcloud-aio-nextcloud IN A +short +search | grep '^[0-9.]\+$' | sort | head -n1)" +HAPROXYFILE="$(sed "s|NC_IPV4_PLACEHOLDER|$IPv4_ADDRESS_NC|" /haproxy.cfg)" +echo "$HAPROXYFILE" > /tmp/haproxy.cfg + +IPv6_ADDRESS_NC="$(dig nextcloud-aio-nextcloud AAAA +short +search | grep '^[0-9a-f:]\+$' | sort | head -n1)" +if [ -n "$IPv6_ADDRESS_NC" ]; then + HAPROXYFILE="$(sed "s|NC_IPV6_PLACEHOLDER|$IPv6_ADDRESS_NC|" /tmp/haproxy.cfg)" +else + HAPROXYFILE="$(sed "s# || { src NC_IPV6_PLACEHOLDER }##g" /tmp/haproxy.cfg)" +fi +echo "$HAPROXYFILE" > /tmp/haproxy.cfg +set +x + +haproxy -f /tmp/haproxy.cfg -db diff --git a/Containers/domaincheck/Dockerfile b/Containers/domaincheck/Dockerfile index ab09e156..374aba4a 100644 --- a/Containers/domaincheck/Dockerfile +++ b/Containers/domaincheck/Dockerfile @@ -1,18 +1,22 @@ -FROM alpine:3.16.2 -RUN apk add --update --no-cache lighttpd bash curl +# syntax=docker/dockerfile:latest +FROM alpine:3.23.3 +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache bash lighttpd netcat-openbsd; \ + adduser -S www-data -G www-data; \ + rm -rf /etc/lighttpd/lighttpd.conf; \ + chmod 777 -R /etc/lighttpd; \ + mkdir -p /var/www/domaincheck; \ + chown www-data:www-data -R /var/www; \ + chmod 777 -R /var/www/domaincheck +COPY --chown=www-data:www-data lighttpd.conf /lighttpd.conf -RUN adduser -S www-data -G www-data -RUN rm -rf /etc/lighttpd/lighttpd.conf -COPY lighttpd.conf /etc/lighttpd/lighttpd.conf -RUN chmod +r -R /etc/lighttpd && \ - chown www-data:www-data -R /var/www && \ - chown www-data:www-data /etc/lighttpd/lighttpd.conf - -COPY start.sh / -RUN chmod +x /start.sh +COPY --chmod=775 start.sh /start.sh USER www-data -RUN mkdir -p /var/www/domaincheck/ ENTRYPOINT ["/start.sh"] -HEALTHCHECK CMD curl -skfI localhost:$APACHE_PORT || exit 1 \ No newline at end of file +HEALTHCHECK CMD nc -z 127.0.0.1 $APACHE_PORT || exit 1 +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/domaincheck/start.sh b/Containers/domaincheck/start.sh index d9186030..06c0aef2 100644 --- a/Containers/domaincheck/start.sh +++ b/Containers/domaincheck/start.sh @@ -11,7 +11,7 @@ if [ -z "$APACHE_PORT" ]; then export APACHE_PORT="443" fi -CONF_FILE="$(sed "s|ipv6-placeholder|\[::\]:$APACHE_PORT|" /etc/lighttpd/lighttpd.conf)" +CONF_FILE="$(sed "s|ipv6-placeholder|\[::\]:$APACHE_PORT|" /lighttpd.conf)" echo "$CONF_FILE" > /etc/lighttpd/lighttpd.conf # Check config file diff --git a/Containers/fulltextsearch/Dockerfile b/Containers/fulltextsearch/Dockerfile index c789a72e..ff1e923f 100644 --- a/Containers/fulltextsearch/Dockerfile +++ b/Containers/fulltextsearch/Dockerfile @@ -1,6 +1,27 @@ +# syntax=docker/dockerfile:latest # Probably from here https://github.com/elastic/elasticsearch/blob/main/distribution/docker/src/docker/Dockerfile -FROM elasticsearch:7.17.6 +FROM elasticsearch:8.19.10 -RUN elasticsearch-plugin install --batch ingest-attachment +USER root -HEALTHCHECK CMD curl -skfI localhost:9200 || exit 1 +ARG DEBIAN_FRONTEND=noninteractive + +# hadolint ignore=DL3008 +RUN set -ex; \ + \ + apt-get update; \ + apt-get upgrade -y; \ + apt-get install -y --no-install-recommends \ + tzdata \ + ; \ + rm -rf /var/lib/apt/lists/*; + +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +USER 1000:0 + +HEALTHCHECK --interval=10s --timeout=5s --start-period=1m --retries=5 CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" +ENV ES_JAVA_OPTS="-Xms512M -Xmx512M" diff --git a/Containers/fulltextsearch/healthcheck.sh b/Containers/fulltextsearch/healthcheck.sh new file mode 100644 index 00000000..5e888ea6 --- /dev/null +++ b/Containers/fulltextsearch/healthcheck.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +nc -z 127.0.0.1 9200 || exit 1 diff --git a/Containers/imaginary/Dockerfile b/Containers/imaginary/Dockerfile index af0347b8..b108ac18 100644 --- a/Containers/imaginary/Dockerfile +++ b/Containers/imaginary/Dockerfile @@ -1,15 +1,47 @@ -# From https://github.com/h2non/imaginary/blob/master/Dockerfile -FROM nextcloud/imaginary:20220822 +# syntax=docker/dockerfile:latest +FROM golang:1.25.6-alpine3.23 AS go + +ENV IMAGINARY_HASH=6a274b488759a896aff02f52afee6e50b5e3a3ee -USER root RUN set -ex; \ - \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - ; \ - rm -rf /var/lib/apt/lists/* -USER nobody + apk upgrade --no-cache -a; \ + apk add --no-cache \ + vips-dev \ + vips-magick \ + vips-heif \ + vips-jxl \ + vips-poppler \ + build-base; \ + go install github.com/h2non/imaginary@"$IMAGINARY_HASH"; -HEALTHCHECK CMD curl -skI 127.0.0.1:9000 || exit 1 \ No newline at end of file +FROM alpine:3.23.3 +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + tzdata \ + ca-certificates \ + netcat-openbsd \ + vips \ + vips-magick \ + vips-heif \ + vips-jxl \ + vips-poppler \ + ttf-dejavu \ + bash + +COPY --from=go /go/bin/imaginary /usr/local/bin/imaginary +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +ENV PORT=9000 + +USER 65534 + +# https://github.com/h2non/imaginary#memory-issues +ENV MALLOC_ARENA_MAX=2 +ENTRYPOINT ["/start.sh"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/imaginary/healthcheck.sh b/Containers/imaginary/healthcheck.sh new file mode 100644 index 00000000..46d700fc --- /dev/null +++ b/Containers/imaginary/healthcheck.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +nc -z 127.0.0.1 "$PORT" || exit 1 diff --git a/Containers/imaginary/start.sh b/Containers/imaginary/start.sh new file mode 100644 index 00000000..2b93da8f --- /dev/null +++ b/Containers/imaginary/start.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +echo "Imaginary has started" +if [ -z "$IMAGINARY_SECRET" ]; then + imaginary -return-size -max-allowed-resolution 222.2 "$@" +else + imaginary -return-size -max-allowed-resolution 222.2 -key "$IMAGINARY_SECRET" "$@" +fi diff --git a/Containers/mastercontainer/.idea/.gitignore b/Containers/mastercontainer/.idea/.gitignore deleted file mode 100644 index 73f69e09..00000000 --- a/Containers/mastercontainer/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/Containers/mastercontainer/.idea/mastercontainer.iml b/Containers/mastercontainer/.idea/mastercontainer.iml deleted file mode 100644 index d6ebd480..00000000 --- a/Containers/mastercontainer/.idea/mastercontainer.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/Containers/mastercontainer/.idea/misc.xml b/Containers/mastercontainer/.idea/misc.xml deleted file mode 100644 index 639900d1..00000000 --- a/Containers/mastercontainer/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Containers/mastercontainer/.idea/modules.xml b/Containers/mastercontainer/.idea/modules.xml deleted file mode 100644 index ff3363f5..00000000 --- a/Containers/mastercontainer/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Containers/mastercontainer/.idea/vcs.xml b/Containers/mastercontainer/.idea/vcs.xml deleted file mode 100644 index b2bdec2d..00000000 --- a/Containers/mastercontainer/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Containers/mastercontainer/Caddyfile b/Containers/mastercontainer/Caddyfile index 646d37e8..da0e222d 100644 --- a/Containers/mastercontainer/Caddyfile +++ b/Containers/mastercontainer/Caddyfile @@ -6,17 +6,32 @@ storage file_system { root /mnt/docker-aio-config/caddy/ } + + log { + level ERROR + } + + servers { + protocols h1 h2 h2c + } + + on_demand_tls { + ask http://127.0.0.1:9876/ + } } http://:80 { - redir https://{host}{uri} + redir https://{host}{uri} permanent } https://:8443 { - reverse_proxy localhost:8000 + reverse_proxy 127.0.0.1:8000 tls { on_demand + issuer acme { + disable_tlsalpn_challenge + } } } diff --git a/Containers/mastercontainer/Dockerfile b/Containers/mastercontainer/Dockerfile index 37442eb2..f3079ca7 100644 --- a/Containers/mastercontainer/Dockerfile +++ b/Containers/mastercontainer/Dockerfile @@ -1,105 +1,138 @@ +# syntax=docker/dockerfile:latest # Docker CLI is a requirement -FROM docker:20.10.17-dind-alpine3.16 as dind +FROM docker:29.2.0-cli AS docker # Caddy is a requirement -FROM caddy:2.5.2-alpine as caddy +FROM caddy:2.10.2-alpine AS caddy -# From https://github.com/docker-library/php/blob/master/8.0/bullseye/apache/Dockerfile -FROM php:8.0.22-apache-bullseye +# From https://github.com/docker-library/php/blob/master/8.4/alpine3.23/fpm/Dockerfile +FROM php:8.4.17-fpm-alpine3.23 EXPOSE 80 EXPOSE 8080 EXPOSE 8443 -RUN mkdir -p /mnt/docker-aio-config/; +# Overwrite home variable for subservices +ENV HOME=/var/www -VOLUME /mnt/docker-aio-config/ +COPY --from=caddy /usr/bin/caddy /usr/bin/caddy +COPY --from=docker /usr/local/bin/docker /usr/local/bin/docker -RUN mkdir -p /var/www/docker-aio; +COPY community-containers /var/www/docker-aio/community-containers +COPY php /var/www/docker-aio/php +COPY --chmod=775 Containers/mastercontainer/*.sh / +COPY --chmod=664 Containers/mastercontainer/Caddyfile /Caddyfile +COPY --chmod=664 Containers/mastercontainer/supervisord.conf /supervisord.conf +COPY Containers/mastercontainer/mastercontainer.conf /etc/apache2/sites-available/mastercontainer.conf WORKDIR /var/www/docker-aio -RUN apt-get update; \ - apt-get install -y --no-install-recommends \ - git \ +# hadolint ignore=SC2086,DL3047,DL3003,DL3004 +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache shadow; \ + groupmod -g 33 www-data; \ + usermod -u 33 -g 33 www-data; \ + \ + apk add --no-cache \ + util-linux-misc \ + ca-certificates \ + wget \ + bash \ + apache2 \ + apache2-proxy \ + apache2-ssl \ supervisor \ openssl \ sudo \ - dpkg-dev \ - netcat \ - ; \ - rm -rf /var/lib/apt/lists/* - -COPY --from=caddy /usr/bin/caddy /usr/bin/ -RUN chmod +x /usr/bin/caddy - -COPY --from=dind /usr/local/bin/docker /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker - -RUN set -ex; \ - pecl install APCu-5.1.21; \ - docker-php-ext-enable apcu - -RUN set -e && \ - curl -sS https://getcomposer.org/installer | php && \ - mv composer.phar /usr/local/bin/composer && \ - chmod +x /usr/local/bin/composer && \ + netcat-openbsd \ + curl \ + grep; \ + \ + apk add --no-cache --virtual .build-deps \ + autoconf \ + build-base; \ + pecl install APCu-5.1.28; \ + docker-php-ext-enable apcu; \ + rm -r /tmp/pear; \ + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )"; \ + apk add --no-cache --virtual .nextcloud-aio-rundeps $runDeps; \ + apk del .build-deps; \ + grep -q '^pm = dynamic' /usr/local/etc/php-fpm.d/www.conf; \ + sed -i 's/^pm = dynamic/pm = ondemand/' /usr/local/etc/php-fpm.d/www.conf; \ + sed -i 's/^pm.max_children =.*/pm.max_children = 80/' /usr/local/etc/php-fpm.d/www.conf; \ + sed -i 's|access.log = /proc/self/fd/2|access.log = /proc/self/fd/1|' /usr/local/etc/php-fpm.d/docker.conf; \ + grep -q ';listen.allowed_clients' /usr/local/etc/php-fpm.d/www.conf; \ + sed -i 's|;listen.allowed_clients.*|listen.allowed_clients = 127.0.0.1,::1|' /usr/local/etc/php-fpm.d/www.conf; \ + \ + apk add --no-cache git; \ + wget https://getcomposer.org/installer -O - | php -- --install-dir=/usr/local/bin --filename=composer; \ + chmod +x /usr/local/bin/composer; \ cd /var/www/docker-aio; \ - git clone https://github.com/nextcloud-releases/all-in-one.git --depth 1 .; \ + rm -r ./php/tests; \ + chown www-data:www-data -R /var/www/docker-aio; \ cd php; \ - composer install --no-dev; \ - composer clearcache; \ + sudo -E -u www-data composer install --no-dev; \ + sudo -E -u www-data composer clear-cache; \ cd ..; \ rm -f /usr/local/bin/composer; \ - chmod 770 -R ./; \ - chown www-data:www-data -R ./; \ - rm -r ./php/data; \ - rm -r ./php/session - -RUN mkdir -p /etc/apache2/certs && \ - cd /etc/apache2/certs && \ - openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 -subj "/C=DE/ST=BE/L=Local/O=Dev/CN=nextcloud.local" -keyout ./ssl.key -out ./ssl.crt; - -COPY mastercontainer.conf /etc/apache2/sites-available/ - -RUN a2enmod rewrite \ - headers \ - env \ - mime \ - dir \ - authz_core \ - proxy \ - proxy_http \ - ssl - -RUN rm /etc/apache2/ports.conf; \ - sed -s -i -e "s/Include ports.conf//" /etc/apache2/apache2.conf; \ - sed -i "/^Listen /d" /etc/apache2/apache2.conf - -RUN a2dissite 000-default && \ - a2dissite default-ssl && \ - a2ensite mastercontainer.conf - -RUN mkdir /var/log/supervisord; \ + chmod -R 770 /var/www/docker-aio; \ + chown -R www-data:www-data /var/www; \ + rm -r php/data; \ + rm -r php/session; \ + \ + mkdir -p /etc/apache2/certs; \ + cd /etc/apache2/certs; \ + openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 -subj "/C=DE/ST=BE/L=Local/O=Dev/CN=nextcloud.local" -keyout /etc/apache2/certs/ssl.key -out /etc/apache2/certs/ssl.crt; \ + \ + sed -i \ + -e '/^Listen /d' \ + -e 's/^LogLevel .*/LogLevel error/' \ + -e 's|^ErrorLog .*|ErrorLog /proc/self/fd/2|' \ + -e 's/User apache/User www-data/g' \ + -e 's/Group apache/Group www-data/g' \ + -e 's/^#\(LoadModule .*mod_rewrite.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_headers.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_env.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_mime.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_dir.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_authz_core.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_mpm_event.so\)/\1/' \ + -e 's/\(LoadModule .*mod_mpm_worker.so\)/#\1/' \ + -e 's/\(LoadModule .*mod_mpm_prefork.so\)/#\1/' \ + -e 's/\(ScriptAlias \)/#\1/' \ + /etc/apache2/httpd.conf; \ + mkdir -p /etc/apache2/logs; \ + rm /etc/apache2/conf.d/ssl.conf; \ + echo "ServerName localhost" | tee -a /etc/apache2/httpd.conf; \ + grep -q '^LoadModule lbmethod_heartbeat_module' /etc/apache2/conf.d/proxy.conf; \ + sed -i 's|^LoadModule lbmethod_heartbeat_module.*|#LoadModule lbmethod_heartbeat_module|' /etc/apache2/conf.d/proxy.conf; \ + echo "SSLSessionCache nonenotnull" | tee -a /etc/apache2/httpd.conf; \ + echo "LoadModule ssl_module modules/mod_ssl.so" | tee -a /etc/apache2/httpd.conf; \ + echo "LoadModule socache_shmcb_module modules/mod_socache_shmcb.so" | tee -a /etc/apache2/httpd.conf; \ + echo "Include /etc/apache2/sites-available/mastercontainer.conf" | tee -a /etc/apache2/httpd.conf; \ + \ + rm -f /etc/apache2/conf.d/default.conf \ + /etc/apache2/conf.d/userdir.conf \ + /etc/apache2/conf.d/info.conf; \ + \ + rm -rf /var/www/localhost/cgi-bin/; \ + mkdir /var/log/supervisord; \ mkdir /var/run/supervisord; -COPY Caddyfile / -COPY start.sh /usr/bin/ -COPY backup-time-file-watcher.sh / -COPY session-deduplicator.sh / -COPY cron.sh / -COPY daily-backup.sh / -COPY supervisord.conf / -RUN chmod +x /usr/bin/start.sh; \ - chmod +x /cron.sh; \ - chmod +x /session-deduplicator.sh; \ - chmod +x /backup-time-file-watcher.sh; \ - chmod +x /daily-backup.sh; \ - chmod a+r /Caddyfile +# hadolint ignore=DL3048 +LABEL org.label-schema.vendor="Nextcloud" \ + wud.watch="false" \ + com.docker.compose.project="nextcloud-aio" +# hadolint ignore=DL3002 USER root -ENTRYPOINT ["start.sh"] -CMD ["/usr/bin/supervisord", "-c", "/supervisord.conf"] +ENTRYPOINT ["/start.sh"] -HEALTHCHECK CMD curl -skfI https://localhost:8080 || exit 1 \ No newline at end of file +HEALTHCHECK CMD /healthcheck.sh diff --git a/Containers/mastercontainer/README.md b/Containers/mastercontainer/README.md new file mode 100644 index 00000000..de6b535d --- /dev/null +++ b/Containers/mastercontainer/README.md @@ -0,0 +1,69 @@ +# Nextcloud All-in-One `mastercontainer` + +This folder contains the OCI/Docker container definition, along with associated resources and +configuration files, for building the `mastercontainer` as part of the Nextcloud All-in-One +project. This container hosts [the Nextcloud AIO interface]( +https://github.com/nextcloud/all-in-one/tree/main/php)[^app], and a dedicated PHP environment +for it (which is completely independent of the Nextcloud Server). + +## Overview + +The mastercontainer acts as the central orchestration service for the deployment and management +of all other containers in the Nextcloud All-in-One stack. It hosts: + +- A dedicated PHP SAPI/backend (php-fpm) for AIO itself (not Nextcloud Server) +- An Apache service for accessing the AIO interface via a self-signed HTTPS VirtualHost on 8080/tcp +- A Caddy reverse proxy service enabling HTTPS access to the AIO frontend on port 8443/tcp. + - Caddy will automatically issue a Let's Encrypt issued certificate if port 80 and 8443 + is open/forwarded and a domain pointer is in place; then, simply open the Nextcloud AIO interface using the + domain (`https://your-domain-that-points-to-this-server.tld:8443`). The Let's Encrypt certificate request will + use an [ACME HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) challenge. +- Miscellaneous support services specific to AIO (backup management, health checks, etc.) + +## Key Responsibilities + +- Orchestrates the deployment and lifecycle of all Nextcloud service containers +- Handles initial setup and container configuration +- Coordinates image updates +- Monitors general system health + +It triggers the initial installation and ensures the smooth operation of the Nextcloud +All-in-One stack. + +## Contents + +- **Dockerfile**: Instructions for building the mastercontainer image. +- **Entrypoint script**: The `start.sh` script is used for container initialization and runtime + configuration before starting supervisord. +- [**Nextcloud All-in-One Controller App**](https://github.com/nextcloud/all-in-one/tree/main/php): The + core AIO orchestrator that handles configuration and settings for the containers. +- **Supervisor**: The `supervisord.conf` file defines the long-running services hosted within + the container (php-fpm, cron, etc.) + +## Usage + +This container should be used as the trigger image when deploying the Nextcloud All-in-One +stack in a Docker or other OCI-compliant container environment. For detailed deployment +instructions, refer to the [project documentation]( +https://github.com/nextcloud/all-in-one). + +## Related Resources + +- [Main Repository](https://github.com/nextcloud/all-in-one) +- [Documentation](https://github.com/nextcloud/all-in-one#readme) + +## Contributing + +Contributions are welcome! Please follow the Nextcloud project's guidelines and submit pull +requests or issues via the main repository. + +## License + +This folder and its contents are licensed under the +[GNU AGPLv3](https://www.gnu.org/licenses/agpl-3.0.html), in line with the rest of Nextcloud +All-in-One. + +[^app]: The Nextcloud All-in-One interface allows users to install, configure, and +manage their Nextcloud instance and related containers via a secure web interface and API. +It automates and simplifies complex tasks such as container orchestration, backups, updates, +and service management for users deploying Nextcloud in Docker environments. diff --git a/Containers/mastercontainer/cron.sh b/Containers/mastercontainer/cron.sh index 1fe5ff8d..67af80e7 100644 --- a/Containers/mastercontainer/cron.sh +++ b/Containers/mastercontainer/cron.sh @@ -12,15 +12,20 @@ while true; do export AUTOMATIC_UPDATES=0 export START_CONTAINERS=1 fi + if [ "$(sed -n '3p' "/mnt/docker-aio-config/data/daily_backup_time")" != 'successNotificationsAreNotEnabled' ]; then + export SEND_SUCCESS_NOTIFICATIONS=1 + else + export SEND_SUCCESS_NOTIFICATIONS=0 + fi set +x + if [ -f "/mnt/docker-aio-config/data/daily_backup_running" ]; then + export LOCK_FILE_PRESENT=1 + else + export LOCK_FILE_PRESENT=0 + fi else export BACKUP_TIME="04:00" export DAILY_BACKUP=0 - fi - - if [ -f "/mnt/docker-aio-config/data/daily_backup_running" ]; then - export LOCK_FILE_PRESENT=1 - else export LOCK_FILE_PRESENT=0 fi @@ -38,14 +43,32 @@ while true; do # Make sure to delete the lock file always rm -f "/mnt/docker-aio-config/data/daily_backup_running" - # Check for updates and send notification if yes - sudo -u www-data php /var/www/docker-aio/php/src/Cron/UpdateNotification.php + # Check for updates and send notification if yes on saturdays + if [ "$(date +%u)" = 6 ]; then + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/UpdateNotification.php + fi + + # Check if AIO is outdated + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/OutdatedNotification.php # Remove sessions older than 24h find "/mnt/docker-aio-config/session/" -mindepth 1 -mmin +1440 -delete + # Remove nextcloud-aio-domaincheck container + if sudo -E -u www-data docker ps --format "{{.Names}}" --filter "status=exited" | grep -q "^nextcloud-aio-domaincheck$"; then + sudo -E -u www-data docker container remove nextcloud-aio-domaincheck + fi + # Remove dangling images - sudo -u www-data docker image prune -f + sudo -E -u www-data docker image prune --filter "label=org.label-schema.vendor=Nextcloud" --force + + # Check for available free space + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/CheckFreeDiskSpace.php + + # Remove mastercontainer from default bridge network + if sudo -E -u www-data docker inspect nextcloud-aio-mastercontainer --format "{{.NetworkSettings.Networks}}" | grep -q "bridge"; then + sudo -E -u www-data docker network disconnect bridge nextcloud-aio-mastercontainer + fi # Wait 60s so that the whole loop will not be executed again sleep 60 diff --git a/Containers/mastercontainer/daily-backup.sh b/Containers/mastercontainer/daily-backup.sh index 09eefec1..89ef3cd5 100644 --- a/Containers/mastercontainer/daily-backup.sh +++ b/Containers/mastercontainer/daily-backup.sh @@ -1,20 +1,42 @@ #!/bin/bash -echo "Daily backup has started" +echo "Daily backup script has started" + +# Check if initial configuration has been done, otherwise this script should do nothing. +CONFIG_FILE=/mnt/docker-aio-config/data/configuration.json +if ! [ -f "$CONFIG_FILE" ] || (! grep -q "wasStartButtonClicked.*1" "$CONFIG_FILE" && ! grep -q "wasStartButtonClicked.*true" "$CONFIG_FILE"); then + echo "Initial configuration via AIO interface not done yet. Exiting..." + exit 0 +fi + +# Daily backup and backup check cannot be run at the same time +if [ "$DAILY_BACKUP" = 1 ] && [ "$CHECK_BACKUP" = 1 ]; then + echo "Daily backup and backup check cannot be run at the same time. Exiting..." + exit 1 +fi # Delete all active sessions and create a lock file # But don't kick out the user if the mastercontainer was just updated since we block the interface either way with the lock file -if [ "$LOCK_FILE_PRESENT" = 0 ]; then - rm -f "/mnt/docker-aio-config/session/"* +if [ "$LOCK_FILE_PRESENT" = 0 ] || ! [ -f "/mnt/docker-aio-config/data/daily_backup_running" ]; then + find "/mnt/docker-aio-config/session/" -mindepth 1 -delete fi -sudo -u www-data touch "/mnt/docker-aio-config/data/daily_backup_running" +sudo -E -u www-data touch "/mnt/docker-aio-config/data/daily_backup_running" # Check if apache is running/stopped, watchtower is stopped and backupcontainer is stopped -APACHE_PORT="$(docker inspect nextcloud-aio-apache --format "{{.HostConfig.PortBindings}}" | grep -oP '[0-9]+' | head -1)" -while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-apache$" && ! nc -z nextcloud-aio-apache "$APACHE_PORT"; do - echo "Waiting for apache to become available" - sleep 30 -done +LOCAL_APACHE_PORT="$(docker inspect nextcloud-aio-apache --format "{{.Config.Env}}" | grep -o 'APACHE_PORT=[0-9]\+' | grep -o '[0-9]\+' | head -1)" +if [ -z "$LOCAL_APACHE_PORT" ]; then + echo "APACHE_PORT is not set which is not expected..." +else + # Connect mastercontainer to nextcloud-aio network to make sure that nextcloud-aio-apache is reachable + # Prevent issues like https://github.com/nextcloud/all-in-one/discussions/5222 + docker network connect nextcloud-aio nextcloud-aio-mastercontainer &>/dev/null + + # Wait for apache to start + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-apache$" && ! nc -z nextcloud-aio-apache "$LOCAL_APACHE_PORT"; do + echo "Waiting for apache to become available" + sleep 30 + done +fi while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-watchtower$"; do echo "Waiting for watchtower to stop" sleep 30 @@ -26,42 +48,71 @@ done # Update the mastercontainer if [ "$AUTOMATIC_UPDATES" = 1 ]; then - sudo -u www-data php /var/www/docker-aio/php/src/Cron/UpdateMastercontainer.php + echo "Starting mastercontainer update..." + echo "(The script might get exited due to that. In order to update all the other containers correctly, you need to run this script with the same settings a second time.)" + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/UpdateMastercontainer.php fi # Wait for watchtower to stop -if [ "$AUTOMATIC_UPDATES" = 1 ] && ! docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-watchtower$"; then - echo "Something seems to be wrong: Watchtower should be started at this step." -else +if [ "$AUTOMATIC_UPDATES" = 1 ]; then + if ! docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-watchtower$"; then + echo "Something seems to be wrong: Watchtower should be started at this step." + fi while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-watchtower$"; do echo "Waiting for watchtower to stop" sleep 30 done fi +# Update container images to reduce downtime later on +if [ "$AUTOMATIC_UPDATES" = 1 ]; then + echo "Updating container images..." + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/PullContainerImages.php +fi + # Stop containers if required -if [ "$DAILY_BACKUP" != 1 ] || [ "$STOP_CONTAINERS" = 1 ]; then - sudo -u www-data php /var/www/docker-aio/php/src/Cron/StopContainers.php +# shellcheck disable=SC2235 +if [ "$CHECK_BACKUP" != 1 ] && ([ "$DAILY_BACKUP" != 1 ] || [ "$STOP_CONTAINERS" = 1 ]); then + echo "Stopping containers..." + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/StopContainers.php fi # Execute the backup itself and some related tasks (also stops the containers) if [ "$DAILY_BACKUP" = 1 ]; then - sudo -u www-data php /var/www/docker-aio/php/src/Cron/CreateBackup.php + echo "Creating daily backup..." + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/CreateBackup.php + if ! docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-borgbackup$"; then + echo "Something seems to be wrong: the borg container should be started at this step." + fi + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-borgbackup$"; do + echo "Waiting for backup container to stop" + sleep 30 + done +fi + +# Execute backup check +if [ "$CHECK_BACKUP" = 1 ]; then + echo "Starting backup check..." + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/CheckBackup.php fi # Start and/or update containers if [ "$AUTOMATIC_UPDATES" = 1 ]; then - sudo -u www-data php /var/www/docker-aio/php/src/Cron/StartAndUpdateContainers.php + echo "Starting and updating containers..." + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/StartAndUpdateContainers.php else if [ "$START_CONTAINERS" = 1 ]; then - sudo -u www-data php /var/www/docker-aio/php/src/Cron/StartContainers.php + echo "Starting containers without updating them..." + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/StartContainers.php fi fi # Delete the lock file rm -f "/mnt/docker-aio-config/data/daily_backup_running" -if [ "$DAILY_BACKUP" = 1 ]; then +# Send backup notification +# shellcheck disable=SC2235 +if [ "$DAILY_BACKUP" = 1 ] && ([ "$AUTOMATIC_UPDATES" = 1 ] || [ "$START_CONTAINERS" = 1 ]); then # Wait for the nextcloud container to start and send if the backup was successful if ! docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-nextcloud$"; then echo "Something seems to be wrong: Nextcloud should be started at this step." @@ -75,7 +126,8 @@ if [ "$DAILY_BACKUP" = 1 ]; then fi done fi - sudo -u www-data php /var/www/docker-aio/php/src/Cron/BackupNotification.php + echo "Sending backup notification..." + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/BackupNotification.php fi -echo "Daily backup has finished" +echo "Daily backup script has finished" diff --git a/Containers/mastercontainer/healthcheck.sh b/Containers/mastercontainer/healthcheck.sh new file mode 100644 index 00000000..72187591 --- /dev/null +++ b/Containers/mastercontainer/healthcheck.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ -f "/mnt/docker-aio-config/data/configuration.json" ]; then + nc -z 127.0.0.1 80 || exit 1 + nc -z 127.0.0.1 8000 || exit 1 + nc -z 127.0.0.1 8080 || exit 1 + nc -z 127.0.0.1 8443 || exit 1 + nc -z 127.0.0.1 9000 || exit 1 + nc -z 127.0.0.1 9876 || exit 1 +fi diff --git a/Containers/mastercontainer/mastercontainer.conf b/Containers/mastercontainer/mastercontainer.conf index 3185f911..7d294694 100644 --- a/Containers/mastercontainer/mastercontainer.conf +++ b/Containers/mastercontainer/mastercontainer.conf @@ -1,8 +1,5 @@ -Listen 8000 -Listen 8080 - -CustomLog ${APACHE_LOG_DIR}/access.log combined -ErrorLog ${APACHE_LOG_DIR}/error.log +Listen 127.0.0.1:8000 +Listen 8080 https # Deny access to .ht files @@ -10,10 +7,19 @@ ErrorLog ${APACHE_LOG_DIR}/error.log # Http host - + + ServerName 127.0.0.1 + + # Add error log + CustomLog /proc/self/fd/1 proxy + LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" proxy + ErrorLog /proc/self/fd/2 + ErrorLogFormat "[%t] [%l] [%E] [client: %{X-Forwarded-For}i] [%M] [%{User-Agent}i]" + LogLevel warn + # PHP match - SetHandler application/x-httpd-php + SetHandler "proxy:fcgi://127.0.0.1:9000" # Master dir DocumentRoot /var/www/docker-aio/php/public/ @@ -35,16 +41,22 @@ ErrorLog ${APACHE_LOG_DIR}/error.log # Https host # Proxy to https - ProxyPass / http://localhost:8000/ - ProxyPassReverse / http://localhost:8000/ + ProxyPass / http://127.0.0.1:8000/ + ProxyPassReverse / http://127.0.0.1:8000/ ProxyPreserveHost On # SSL SSLCertificateKeyFile /etc/apache2/certs/ssl.key SSLCertificateFile /etc/apache2/certs/ssl.crt SSLEngine on SSLProtocol -all +TLSv1.2 +TLSv1.3 + SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 + SSLHonorCipherOrder off + SSLSessionTickets off # Increase timeout in case e.g. the initial download takes a long time Timeout 7200 ProxyTimeout 7200 + +# See https://httpd.apache.org/docs/trunk/mod/core.html#traceenable +TraceEnable Off diff --git a/Containers/mastercontainer/session-deduplicator.sh b/Containers/mastercontainer/session-deduplicator.sh index 4326090a..08ec0f9c 100644 --- a/Containers/mastercontainer/session-deduplicator.sh +++ b/Containers/mastercontainer/session-deduplicator.sh @@ -1,23 +1,22 @@ #!/bin/bash +deduplicate_sessions() { + echo "Deleting duplicate sessions" + find "/mnt/docker-aio-config/session/" -mindepth 1 -exec grep -qv "$NEW_SESSION_TIME" {} \; -delete +} + +compare_times() { + if [ -f "/mnt/docker-aio-config/data/session_date_file" ]; then + unset NEW_SESSION_TIME + NEW_SESSION_TIME="$(cat "/mnt/docker-aio-config/data/session_date_file")" + if [ -n "$NEW_SESSION_TIME" ] && [ -n "$OLD_SESSION_TIME" ] && [ "$NEW_SESSION_TIME" != "$OLD_SESSION_TIME" ]; then + deduplicate_sessions + fi + OLD_SESSION_TIME="$NEW_SESSION_TIME" + fi +} + while true; do - while [ "$(find "/mnt/docker-aio-config/session/" -mindepth 1 -exec grep "aio_authenticated|[a-z]:1" {} \; | wc -l)" -gt 1 ]; do - unset SESSION_FILES - SESSION_FILES="$(find "/mnt/docker-aio-config/session/" -mindepth 1)" - unset SESSION_FILES_ARRAY - mapfile -t SESSION_FILES_ARRAY <<< "$SESSION_FILES" - for SESSION_FILE in "${SESSION_FILES_ARRAY[@]}"; do - if ! grep -q "aio_authenticated|[a-z]:1" "$SESSION_FILE"; then - rm "$SESSION_FILE" - fi - done - echo "Deleting duplicate sessions" - unset OLDEST_FILE - set -x - # shellcheck disable=SC2012 - OLDEST_FILE="$(ls -t "/mnt/docker-aio-config/session/" | tail -1)" - rm "/mnt/docker-aio-config/session/$OLDEST_FILE" - set +x - done - sleep 5 + compare_times + sleep 2 done diff --git a/Containers/mastercontainer/start.sh b/Containers/mastercontainer/start.sh old mode 100755 new mode 100644 index 38c38975..a65e29ae --- a/Containers/mastercontainer/start.sh +++ b/Containers/mastercontainer/start.sh @@ -6,6 +6,12 @@ print_green() { printf "%b%s%b\n" "\e[0;92m" "$TEXT" "\e[0m" } +# Function to show text in red +print_red() { + local TEXT="$1" + printf "%b%s%b\n" "\e[0;31m" "$TEXT" "\e[0m" +} + # Function to check if number was provided check_if_number() { case "${1}" in @@ -14,18 +20,42 @@ case "${1}" in esac } +# Check if running as root user +if [ "$EUID" != "0" ]; then + print_red "Container does not run as root user. This is not supported." + exit 1 +fi + +# Check that the CMD is not overwritten nor set +if [ "$*" != "" ]; then + print_red "Docker run command for AIO is incorrect as a CMD option was given which is not expected." + exit 1 +fi + # Check if socket is available and readable -if ! [ -a "/var/run/docker.sock" ]; then - echo "Docker socket is not available. Cannot continue." +if ! [ -e "/var/run/docker.sock" ]; then + print_red "Docker socket is not available. Cannot continue." + echo "Please make sure to mount the docker socket into /var/run/docker.sock inside the container!" + echo "If you did this by purpose because you don't want the container to have access to the docker socket, see https://github.com/nextcloud/all-in-one/tree/main/manual-install." + echo "And https://github.com/nextcloud/all-in-one/blob/main/manual-install/latest.yml" exit 1 elif ! mountpoint -q "/mnt/docker-aio-config"; then - echo "/mnt/docker-aio-config is not a mountpoint. Cannot proceed!" + print_red "/mnt/docker-aio-config is not a mountpoint. Cannot proceed!" + echo "Please make sure to mount the nextcloud_aio_mastercontainer docker volume into /mnt/docker-aio-config inside the container!" + echo "If you are on TrueNas SCALE, see https://github.com/nextcloud/all-in-one#can-i-run-aio-on-truenas-scale" exit 1 -elif ! sudo -u www-data test -r /var/run/docker.sock; then +elif mountpoint -q /var/www/docker-aio/php/containers.json; then + print_red "/var/www/docker-aio/php/containers.json is a mountpoint. Cannot proceed!" + echo "This is a not-supported customization of the mastercontainer!" + echo "Please remove this bind-mount from the mastercontainer." + echo "If you need to customize things, feel free to use https://github.com/nextcloud/all-in-one/tree/main/manual-install" + echo "See https://github.com/nextcloud/all-in-one/blob/main/manual-install/latest.yml" + exit 1 +elif ! sudo -E -u www-data test -r /var/run/docker.sock; then echo "Trying to fix docker.sock permissions internally..." DOCKER_GROUP=$(stat -c '%G' /var/run/docker.sock) DOCKER_GROUP_ID=$(stat -c '%g' /var/run/docker.sock) - # Check if a group with the same group id of /var/run/docker.socket already exists in the container + # Check if a group with the same group name of /var/run/docker.socket already exists in the container if grep -q "^$DOCKER_GROUP:" /etc/group; then # If yes, add www-data to that group echo "Adding internal www-data to group $DOCKER_GROUP" @@ -39,25 +69,68 @@ elif ! sudo -u www-data test -r /var/run/docker.sock; then groupadd -g "$DOCKER_GROUP_ID" docker usermod -aG docker www-data fi - if ! sudo -u www-data test -r /var/run/docker.sock; then - echo "Docker socket is not readable by the www-data user. Cannot continue." + if ! sudo -E -u www-data test -r /var/run/docker.sock; then + print_red "Docker socket is not readable by the www-data user. Cannot continue." exit 1 fi fi -# Check if api version is supported -if ! sudo -u www-data docker info &>/dev/null; then - echo "Cannot connect to the docker socket. Cannot proceed." +# Get default docker api version +API_VERSION_FILE="$(find ./ -name DockerActionManager.php | head -1)" +API_VERSION="$(grep -oP 'const string API_VERSION.*\;' "$API_VERSION_FILE" | grep -oP '[0-9]+.[0-9]+' | head -1)" +if [ -z "$API_VERSION" ]; then + print_red "Could not get API_VERSION. Something is wrong!" exit 1 fi -API_VERSION_FILE="$(find ./ -name DockerActionManager.php | head -1)" -API_VERSION="$(grep -oP 'const API_VERSION.*\;' "$API_VERSION_FILE" | grep -oP '[0-9]+.[0-9]+' | head -1)" + +# Check if DOCKER_API_VERSION is set globally +if [ -n "$DOCKER_API_VERSION" ]; then + if ! echo "$DOCKER_API_VERSION" | grep -q '^[0-9].[0-9]\+$'; then + print_red "You've set DOCKER_API_VERSION but not to an allowed value. +The string must be a version number like e.g. '1.44'. +It is set to '$DOCKER_API_VERSION'." + exit 1 + fi + print_red "DOCKER_API_VERSION was found to be set to '$DOCKER_API_VERSION'." + print_red "Please note that only v$API_VERSION is officially supported and tested by the maintainers of Nextcloud AIO." + print_red "So you run on your own risk and things might break without warning." +else + # Export docker api version to use it everywhere + export DOCKER_API_VERSION="$API_VERSION" +fi + +# Set a fallback docker api version. Needed for api version check. +# The check will not work otherwise on old docker versions +FALLBACK_DOCKER_API_VERSION="1.41" + +# Check if docker info can be used +if ! sudo -E -u www-data docker info &>/dev/null; then + if ! sudo -E -u www-data DOCKER_API_VERSION="$FALLBACK_DOCKER_API_VERSION" docker info &>/dev/null; then + print_red "Cannot connect to the docker socket. Cannot proceed." + echo "Did you maybe remove group read permissions for the docker socket? AIO needs them in order to access the docker socket." + echo "If SELinux is enabled on your host, see https://github.com/nextcloud/all-in-one#are-there-known-problems-when-selinux-is-enabled" + echo "If you are on TrueNas SCALE, see https://github.com/nextcloud/all-in-one#can-i-run-aio-on-truenas-scale" + echo "On macOS, see https://github.com/nextcloud/all-in-one#how-to-run-aio-on-macos" + echo "Another possibility might be that Docker api v$API_VERSION is not supported by your docker daemon." + echo "In that case, you should report this to https://github.com/nextcloud/all-in-one/issues" + echo "" + exit 1 + fi +fi + +# Docker api version check # shellcheck disable=SC2001 -API_VERSION_NUMB="$(echo "$API_VERSION" | sed 's/\.//')" -LOCAL_API_VERSION_NUMB="$(sudo -u www-data docker version | grep -i "api version" | grep -oP '[0-9]+.[0-9]+' | head -1 | sed 's/\.//')" +API_VERSION_NUMB="$(echo "$DOCKER_API_VERSION" | sed 's/\.//')" +LOCAL_API_VERSION_NUMB="$(sudo -E -u www-data docker version | grep -i "api version" | grep -oP '[0-9]+.[0-9]+' | head -1 | sed 's/\.//')" +if [ -z "$LOCAL_API_VERSION_NUMB" ]; then + LOCAL_API_VERSION_NUMB="$(sudo -E -u www-data DOCKER_API_VERSION="$FALLBACK_DOCKER_API_VERSION" docker version | grep -i "api version" | grep -oP '[0-9]+.[0-9]+' | head -1 | sed 's/\.//')" +fi if [ -n "$LOCAL_API_VERSION_NUMB" ] && [ -n "$API_VERSION_NUMB" ]; then if ! [ "$LOCAL_API_VERSION_NUMB" -ge "$API_VERSION_NUMB" ]; then - echo "Docker API v$API_VERSION is not supported by your docker engine. Cannot proceed. Please upgrade your docker engine if you want to run Nextcloud AIO!" + print_red "Docker API v$DOCKER_API_VERSION is not supported by your docker engine. Cannot proceed. Please upgrade your docker engine if you want to run Nextcloud AIO!" + echo "Alternatively, set the DOCKER_API_VERSION environmental variable to a compatible version." + echo "However please note that only v$API_VERSION is officially supported and tested by the maintainers of Nextcloud AIO." + echo "See https://github.com/nextcloud/all-in-one#how-to-adjust-the-internally-used-docker-api-version" exit 1 fi else @@ -65,48 +138,71 @@ else sleep 10 fi +# Check Storage drivers +STORAGE_DRIVER="$(sudo -E -u www-data docker info | grep "Storage Driver")" +# Check if vfs is used: https://github.com/nextcloud/all-in-one/discussions/1467 +if echo "$STORAGE_DRIVER" | grep -q vfs; then + echo "$STORAGE_DRIVER" + print_red "Warning: It seems like the storage driver vfs is used. This will lead to problems with disk space and performance and is disrecommended!" +elif echo "$STORAGE_DRIVER" | grep -q fuse-overlayfs; then + echo "$STORAGE_DRIVER" + print_red "Warning: It seems like the storage driver fuse-overlayfs is used. Please check if you can switch to overlay2 instead." +fi + +# Check if snap install +if sudo -E -u www-data docker info | grep "Docker Root Dir" | grep "/var/snap/docker/"; then + print_red "Warning: It looks like your installation uses docker installed via snap." + print_red "This comes with some limitations and is disrecommended by the docker maintainers." + print_red "See for example https://github.com/nextcloud/all-in-one/discussions/4890#discussioncomment-10386752" +fi + # Check if startup command was executed correctly -if ! sudo -u www-data docker ps | grep -q "nextcloud-aio-mastercontainer"; then - echo "It seems like you did not give the mastercontainer the correct name? -Using a different name is not supported!" +if ! sudo -E -u www-data docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-mastercontainer$"; then + print_red "It seems like you did not give the mastercontainer the correct name? (The 'nextcloud-aio-mastercontainer' container was not found.) +Using a different name is not supported since mastercontainer updates will not work in that case! +If you are on docker swarm and try to run AIO, see https://github.com/nextcloud/all-in-one#can-i-run-this-with-docker-swarm" exit 1 -elif ! sudo -u www-data docker volume ls | grep -q "nextcloud_aio_mastercontainer"; then - echo "It seems like you did not give the mastercontainer volume the correct name? -Using a different name is not supported!" +elif ! sudo -E -u www-data docker volume ls --format "{{.Name}}" | grep -q "^nextcloud_aio_mastercontainer$"; then + print_red "It seems like you did not give the mastercontainer volume the correct name? (The 'nextcloud_aio_mastercontainer' volume was not found.) +Using a different name is not supported since the built-in backup solution will not work in that case!" + exit 1 +elif ! sudo -E -u www-data docker inspect nextcloud-aio-mastercontainer --format '{{.Mounts}}' | grep -q " nextcloud_aio_mastercontainer "; then + print_red "It seems like you did not attach the 'nextcloud_aio_mastercontainer' volume to the mastercontainer? +This is not supported since the built-in backup solution will not work in that case!" exit 1 fi # Check for other options if [ -n "$NEXTCLOUD_DATADIR" ]; then if [ "$NEXTCLOUD_DATADIR" = "nextcloud_aio_nextcloud_datadir" ]; then - echo "NEXTCLOUD_DATADIR is set to $NEXTCLOUD_DATADIR" + sleep 1 elif ! echo "$NEXTCLOUD_DATADIR" | grep -q "^/" || [ "$NEXTCLOUD_DATADIR" = "/" ]; then - echo "You've set NEXTCLOUD_DATADIR but not to an allowed value. -The string must start with '/' and must not be equal to '/'. + print_red "You've set NEXTCLOUD_DATADIR but not to an allowed value. +The string must start with '/' and must not be equal to '/'. Also allowed is 'nextcloud_aio_nextcloud_datadir'. It is set to '$NEXTCLOUD_DATADIR'." exit 1 fi fi if [ -n "$NEXTCLOUD_MOUNT" ]; then if ! echo "$NEXTCLOUD_MOUNT" | grep -q "^/" || [ "$NEXTCLOUD_MOUNT" = "/" ]; then - echo "You've set NEXCLOUD_MOUNT but not to an allowed value. + print_red "You've set NEXTCLOUD_MOUNT but not to an allowed value. The string must start with '/' and must not be equal to '/'. It is set to '$NEXTCLOUD_MOUNT'." exit 1 elif [ "$NEXTCLOUD_MOUNT" = "/mnt/ncdata" ] || echo "$NEXTCLOUD_MOUNT" | grep -q "^/mnt/ncdata/"; then - echo "'/mnt/ncdata' and '/mnt/ncdata/' are not allowed as values for NEXTCLOUD_MOUNT." + print_red "'/mnt/ncdata' and '/mnt/ncdata/' are not allowed as values for NEXTCLOUD_MOUNT." exit 1 fi fi if [ -n "$NEXTCLOUD_DATADIR" ] && [ -n "$NEXTCLOUD_MOUNT" ]; then if [ "$NEXTCLOUD_DATADIR" = "$NEXTCLOUD_MOUNT" ]; then - echo "NEXTCLOUD_DATADIR and NEXTCLOUD_MOUNT are not allowed to be equal." + print_red "NEXTCLOUD_DATADIR and NEXTCLOUD_MOUNT are not allowed to be equal." exit 1 fi fi if [ -n "$NEXTCLOUD_UPLOAD_LIMIT" ]; then if ! echo "$NEXTCLOUD_UPLOAD_LIMIT" | grep -q '^[0-9]\+G$'; then - echo "You've set NEXTCLOUD_UPLOAD_LIMIT but not to an allowed value. + print_red "You've set NEXTCLOUD_UPLOAD_LIMIT but not to an allowed value. The string must start with a number and end with 'G'. It is set to '$NEXTCLOUD_UPLOAD_LIMIT'." exit 1 @@ -114,64 +210,153 @@ It is set to '$NEXTCLOUD_UPLOAD_LIMIT'." fi if [ -n "$NEXTCLOUD_MAX_TIME" ]; then if ! echo "$NEXTCLOUD_MAX_TIME" | grep -q '^[0-9]\+$'; then - echo "You've set NEXTCLOUD_MAX_TIME but not to an allowed value. + print_red "You've set NEXTCLOUD_MAX_TIME but not to an allowed value. The string must be a number. E.g. '3600'. It is set to '$NEXTCLOUD_MAX_TIME'." exit 1 fi fi +if [ -n "$NEXTCLOUD_MEMORY_LIMIT" ]; then + if ! echo "$NEXTCLOUD_MEMORY_LIMIT" | grep -q '^[0-9]\+M$'; then + print_red "You've set NEXTCLOUD_MEMORY_LIMIT but not to an allowed value. +The string must start with a number and end with 'M'. +It is set to '$NEXTCLOUD_MEMORY_LIMIT'." + exit 1 + fi +fi if [ -n "$APACHE_PORT" ]; then if ! check_if_number "$APACHE_PORT"; then - echo "You provided an Apache port but did not only use numbers. + print_red "You provided an Apache port but did not only use numbers. It is set to '$APACHE_PORT'." exit 1 elif ! [ "$APACHE_PORT" -le 65535 ] || ! [ "$APACHE_PORT" -ge 1 ]; then - echo "The provided Apache port is invalid. It must be between 1 and 65535" + print_red "The provided Apache port is invalid. It must be between 1 and 65535" exit 1 fi fi if [ -n "$APACHE_IP_BINDING" ]; then - if ! echo "$APACHE_IP_BINDING" | grep -q '^[0-9.]\+$'; then - echo "You provided an ip-address for the apache container's ip-binding but it was not a valid ip-address. + if ! echo "$APACHE_IP_BINDING" | grep -q '^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$\|^[0-9a-f:]\+$\|^@INTERNAL$'; then + print_red "You provided an ip-address for the apache container's ip-binding but it was not a valid ip-address. It is set to '$APACHE_IP_BINDING'." exit 1 fi fi +if [ -n "$APACHE_ADDITIONAL_NETWORK" ]; then + if ! echo "$APACHE_ADDITIONAL_NETWORK" | grep -q "^[a-zA-Z0-9._-]\+$"; then + print_red "You've set APACHE_ADDITIONAL_NETWORK but not to an allowed value. +It needs to be a string with letters, numbers, hyphens and underscores. +It is set to '$APACHE_ADDITIONAL_NETWORK'." + exit 1 + fi +fi if [ -n "$TALK_PORT" ]; then if ! check_if_number "$TALK_PORT"; then - echo "You provided an Talk port but did not only use numbers. + print_red "You provided an Talk port but did not only use numbers. It is set to '$TALK_PORT'." exit 1 elif ! [ "$TALK_PORT" -le 65535 ] || ! [ "$TALK_PORT" -ge 1 ]; then - echo "The provided Talk port is invalid. It must be between 1 and 65535" + print_red "The provided Talk port is invalid. It must be between 1 and 65535" exit 1 fi fi if [ -n "$APACHE_PORT" ] && [ -n "$TALK_PORT" ]; then if [ "$APACHE_PORT" = "$TALK_PORT" ]; then - echo "APACHE_PORT and TALK_PORT are not allowed to be equal." + print_red "APACHE_PORT and TALK_PORT are not allowed to be equal." exit 1 fi fi -if [ -n "$DOCKER_SOCKET_PATH" ]; then - if ! echo "$DOCKER_SOCKET_PATH" | grep -q "^/" || echo "$DOCKER_SOCKET_PATH" | grep -q "/$"; then - echo "You've set DOCKER_SOCKET_PATH but not to an allowed value. +if [ -n "$WATCHTOWER_DOCKER_SOCKET_PATH" ]; then + if ! echo "$WATCHTOWER_DOCKER_SOCKET_PATH" | grep -q "^/" || echo "$WATCHTOWER_DOCKER_SOCKET_PATH" | grep -q "/$"; then + print_red "You've set WATCHTOWER_DOCKER_SOCKET_PATH but not to an allowed value. The string must start with '/' and must not end with '/'. -It is set to '$DOCKER_SOCKET_PATH'." +It is set to '$WATCHTOWER_DOCKER_SOCKET_PATH'." exit 1 fi fi +if [ -n "$NEXTCLOUD_TRUSTED_CACERTS_DIR" ]; then + if ! echo "$NEXTCLOUD_TRUSTED_CACERTS_DIR" | grep -q "^/" || echo "$NEXTCLOUD_TRUSTED_CACERTS_DIR" | grep -q "/$"; then + print_red "You've set NEXTCLOUD_TRUSTED_CACERTS_DIR but not to an allowed value. +It should be an absolute path to a directory that starts with '/' but not end with '/'. +It is set to '$NEXTCLOUD_TRUSTED_CACERTS_DIR '." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_STARTUP_APPS" ]; then + if ! echo "$NEXTCLOUD_STARTUP_APPS" | grep -q "^[a-z0-9 _-]\+$"; then + print_red "You've set NEXTCLOUD_STARTUP_APPS but not to an allowed value. +It needs to be a string. Allowed are small letters a-z, 0-9, spaces, hyphens and '_'. +It is set to '$NEXTCLOUD_STARTUP_APPS'." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_ADDITIONAL_APKS" ]; then + if ! echo "$NEXTCLOUD_ADDITIONAL_APKS" | grep -q "^[a-z0-9 ._-]\+$"; then + print_red "You've set NEXTCLOUD_ADDITIONAL_APKS but not to an allowed value. +It needs to be a string. Allowed are small letters a-z, digits 0-9, spaces, hyphens, dots and '_'. +It is set to '$NEXTCLOUD_ADDITIONAL_APKS'." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS" ]; then + if ! echo "$NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS" | grep -q "^[a-z0-9 ._-]\+$"; then + print_red "You've set NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS but not to an allowed value. +It needs to be a string. Allowed are small letters a-z, digits 0-9, spaces, hyphens, dots and '_'. +It is set to '$NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS'." + exit 1 + fi +fi +if [ -n "$AIO_COMMUNITY_CONTAINERS" ]; then + print_red "You've set AIO_COMMUNITY_CONTAINERS but the option was removed. +The community containers get managed via the AIO interface now." +fi -# Check DNS resolution -# Prevents issues like https://github.com/nextcloud/all-in-one/discussions/565 -curl https://nextcloud.com &>/dev/null -if [ "$?" = 6 ]; then - echo "Could not resolve the host nextcloud.com." - echo "Most likely the DNS resolving does not work." - echo "You should be able to fix this by adding the '--dns=\"ip.address.of.dns.server\"' option to the docker run command." +# Check if ghcr.io is reachable +# Solves issues like https://github.com/nextcloud/all-in-one/discussions/5268 +if ! curl --no-progress-meter https://ghcr.io/v2/ >/dev/null; then + print_red "Could not reach https://ghcr.io." + echo "Most likely is something blocking access to it." + echo "You should be able to fix this by following https://dockerlabs.collabnix.com/intermediate/networking/Configuring_DNS.html" + echo "Another solution is using https://github.com/nextcloud/all-in-one/tree/main/manual-install" + echo "See https://github.com/nextcloud/all-in-one/blob/main/manual-install/latest.yml" exit 1 fi +# Check that no changes have been made to timezone settings since AIO only supports running in Etc/UTC timezone +if [ -n "$TZ" ]; then + print_red "The environmental variable TZ has been set which is not supported by AIO since it only supports running in the default Etc/UTC timezone!" + echo "The correct timezone can be set in the AIO interface later on!" + # Disable exit since it seems to be by default set on unraid and we dont want to break these instances + # exit 1 +fi +# Check that http proxy or no_proxy variable is not set which AIO does not support +if [ -n "$HTTP_PROXY" ] || [ -n "$http_proxy" ] || [ -n "$HTTPS_PROXY" ] || [ -n "$https_proxy" ] || [ -n "$NO_PROXY" ] || [ -n "$no_proxy" ]; then + print_red "The environmental variable HTTP_PROXY, http_proxy, HTTPS_PROXY, https_proxy, NO_PROXY or no_proxy has been set which is not supported by AIO." + echo "If you need this, then you should use https://github.com/nextcloud/all-in-one/tree/main/manual-install" + echo "See https://github.com/nextcloud/all-in-one/blob/main/manual-install/latest.yml" + exit 1 +fi +if mountpoint -q /etc/localtime; then + print_red "/etc/localtime has been mounted into the container which is not allowed because AIO only supports running in the default Etc/UTC timezone!" + echo "The correct timezone can be set in the AIO interface later on!" + exit 1 +fi +if mountpoint -q /etc/timezone; then + print_red "/etc/timezone has been mounted into the container which is not allowed because AIO only supports running in the default Etc/UTC timezone!" + echo "The correct timezone can be set in the AIO interface later on!" + exit 1 +fi + +# Check if unsupported env are set (but don't exit as it would break many instances) +if [ -n "$APACHE_DISABLE_REWRITE_IP" ]; then + print_red "The environmental variable APACHE_DISABLE_REWRITE_IP has been set which is not supported by AIO. Please remove it!" +fi +if [ -n "$NEXTCLOUD_TRUSTED_DOMAINS" ]; then + print_red "The environmental variable NEXTCLOUD_TRUSTED_DOMAINS has been set which is not supported by AIO. Please remove it!" +fi +if [ -n "$TRUSTED_PROXIES" ]; then + print_red "The environmental variable TRUSTED_PROXIES has been set which is not supported by AIO. Please remove it!" +fi + # Add important folders mkdir -p /mnt/docker-aio-config/data/ mkdir -p /mnt/docker-aio-config/session/ @@ -188,8 +373,8 @@ chown root:root -R /mnt/docker-aio-config/certs/ # Don't allow access to the AIO interface from the Nextcloud container # Probably more cosmetic than anything but at least an attempt -if ! grep -q '# nextcloud-aio-block' /etc/apache2/apache2.conf; then - cat << APACHE_CONF >> /etc/apache2/apache2.conf +if ! grep -q '# nextcloud-aio-block' /etc/apache2/httpd.conf; then + cat << APACHE_CONF >> /etc/apache2/httpd.conf # nextcloud-aio-block-start order allow,deny @@ -216,14 +401,30 @@ if [ -f ./ssl.crt ] && [ -f ./ssl.key ]; then cp "$GENERATED_CERTS/ssl.key" ./ fi -print_green "Initial startup of Nextcloud All In One complete! +print_green "Initial startup of Nextcloud All-in-One complete! You should be able to open the Nextcloud AIO Interface now on port 8080 of this server! E.g. https://internal.ip.of.this.server:8080 +⚠️ Important: do always use an ip-address if you access this port and not a domain as HSTS might block access to it later! -If your server has port 80 and 8443 open and you point a domain to your server, you can get a valid certificate automatially by opening the Nextcloud AIO Interface via: +If your server has port 80 and 8443 open and you point a domain to your server, you can get a valid certificate automatically by opening the Nextcloud AIO Interface via: https://your-domain-that-points-to-this-server.tld:8443" -# Set the timezone to UTC -export TZ=UTC +# Set the timezone to Etc/UTC +export TZ=Etc/UTC -exec "$@" +# Fix apache startup +rm -f /var/run/apache2/httpd.pid + +# Fix caddy startup +if [ -d "/mnt/docker-aio-config/caddy/locks" ]; then + rm -rf /mnt/docker-aio-config/caddy/locks/* +fi + +# Fix the Caddyfile format +caddy fmt --overwrite /Caddyfile + +# Fix caddy log +chmod 777 /root + +# Start supervisord +exec /usr/bin/supervisord -c /supervisord.conf diff --git a/Containers/mastercontainer/supervisord.conf b/Containers/mastercontainer/supervisord.conf index ad24f412..fa5d0845 100644 --- a/Containers/mastercontainer/supervisord.conf +++ b/Containers/mastercontainer/supervisord.conf @@ -1,26 +1,36 @@ [supervisord] nodaemon=true -nodaemon=true logfile=/var/log/supervisord/supervisord.log pidfile=/var/run/supervisord/supervisord.pid childlogdir=/var/log/supervisord/ logfile_maxbytes=50MB logfile_backups=10 loglevel=error +user=root -[program:apache] -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 +[program:php-fpm] +# Stdout logging is disabled as otherwise the logs are spammed +stdout_logfile=NONE stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -command=apache2-foreground +command=php-fpm +user=root + +[program:apache] +# Stdout logging is disabled as otherwise the logs are spammed +stdout_logfile=NONE +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=httpd -DFOREGROUND +user=root [program:caddy] stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -command=sudo -u www-data /usr/bin/caddy run -config /Caddyfile +command=/usr/bin/caddy run --config /Caddyfile +user=www-data [program:cron] stdout_logfile=/dev/stdout @@ -28,6 +38,7 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 command=/cron.sh +user=root [program:backup-time-file-watcher] stdout_logfile=/dev/stdout @@ -35,6 +46,7 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 command=/backup-time-file-watcher.sh +user=root [program:session-deduplicator] stdout_logfile=/dev/stdout @@ -42,3 +54,11 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 command=/session-deduplicator.sh +user=root + +[program:domain-validator] +# Logging is disabled as otherwise all attempts will be logged which spams the logs +stdout_logfile=NONE +stderr_logfile=NONE +command=php -S 127.0.0.1:9876 /var/www/docker-aio/php/domain-validator.php +user=www-data diff --git a/Containers/nextcloud/Dockerfile b/Containers/nextcloud/Dockerfile index 02267c1b..c6d9bf7e 100644 --- a/Containers/nextcloud/Dockerfile +++ b/Containers/nextcloud/Dockerfile @@ -1,72 +1,95 @@ -# From https://github.com/nextcloud/docker/blob/master/23/fpm-alpine/Dockerfile -FROM php:8.0.22-fpm-alpine3.16 +# syntax=docker/dockerfile:latest +FROM php:8.3.30-fpm-alpine3.23 + +ENV PHP_MEMORY_LIMIT=512M +ENV PHP_UPLOAD_LIMIT=16G +ENV PHP_MAX_TIME=3600 +ENV SOURCE_LOCATION=/usr/src/nextcloud +ENV REDIS_DB_INDEX=0 + +# AIO settings start # Do not remove or change this line! +ENV NEXTCLOUD_VERSION=32.0.5 +ENV AIO_TOKEN=123456 +ENV AIO_URL=localhost +# AIO settings end # Do not remove or change this line! + +COPY --chmod=775 Containers/nextcloud/*.sh / +COPY --chmod=774 Containers/nextcloud/upgrade.exclude /upgrade.exclude +COPY Containers/nextcloud/config/*.php / +COPY Containers/nextcloud/supervisord.conf /supervisord.conf + +# AIO cloning start # Do not remove or change this line! +COPY app /usr/src/nextcloud/apps/nextcloud-aio +COPY Containers/nextcloud/root.motd /root.motd +# AIO cloning end # Do not remove or change this line! + +VOLUME /mnt/ncdata +VOLUME /var/www/html # Custom: change id of www-data user as it needs to be the same like on old installations +# hadolint ignore=SC2086,DL3003 RUN set -ex; \ + apk upgrade --no-cache -a; \ apk add --no-cache shadow; \ deluser www-data; \ - groupmod -g 333 xfs; \ - usermod -u 333 -g 333 xfs; \ addgroup -g 33 -S www-data; \ - adduser -u 33 -D -S -G www-data www-data - -# entrypoint.sh and cron.sh dependencies -RUN set -ex; \ + adduser -u 33 -D -S -G www-data www-data; \ \ +# entrypoint.sh and cron.sh dependencies apk add --no-cache \ rsync \ - ; - + ; \ # install the PHP extensions we need # see https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html -ENV PHP_MEMORY_LIMIT 512M -ENV PHP_UPLOAD_LIMIT 10G -ENV PHP_MAX_TIME 3600 -RUN set -ex; \ - \ apk add --no-cache --virtual .build-deps \ $PHPIZE_DEPS \ autoconf \ freetype-dev \ + gmp-dev \ icu-dev \ + imagemagick-dev \ + imagemagick-svg \ + imagemagick-heic \ + imagemagick-tiff \ libevent-dev \ libjpeg-turbo-dev \ libmcrypt-dev \ - libpng-dev \ libmemcached-dev \ + libpng-dev \ + libwebp-dev \ libxml2-dev \ libzip-dev \ openldap-dev \ pcre-dev \ postgresql-dev \ - imagemagick-dev \ - libwebp-dev \ - gmp-dev \ ; \ \ docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \ + docker-php-ext-configure ftp --with-openssl-dir=/usr; \ docker-php-ext-configure ldap; \ docker-php-ext-install -j "$(nproc)" \ bcmath \ exif \ gd \ + gmp \ intl \ ldap \ opcache \ pcntl \ - pdo_mysql \ pdo_pgsql \ + sysvsem \ zip \ - gmp \ ; \ \ # pecl will claim success even if one install fails, so we need to perform each install separately - pecl install APCu-5.1.21; \ - pecl install memcached-3.2.0; \ - pecl install redis-5.3.7; \ - pecl install imagick-3.7.0; \ + pecl install -o igbinary-3.2.16; \ + pecl install APCu-5.1.28; \ + pecl install -D 'enable-memcached-igbinary="yes"' memcached-3.4.0; \ + pecl install -oD 'enable-redis-igbinary="yes" enable-redis-zstd="yes" enable-redis-lz4="yes"' redis-6.3.0; \ + pecl install -o imagick-3.8.1; \ \ docker-php-ext-enable \ + igbinary \ apcu \ memcached \ redis \ @@ -80,43 +103,61 @@ RUN set -ex; \ | sort -u \ | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ )"; \ - apk add --virtual .nextcloud-phpext-rundeps $runDeps; \ - apk del .build-deps - + apk add --no-cache --virtual .nextcloud-phpext-rundeps $runDeps; \ + apk del .build-deps; \ + \ + { \ + echo 'apc.serializer=igbinary'; \ + echo 'session.serialize_handler=igbinary'; \ + } >> /usr/local/etc/php/conf.d/docker-php-ext-igbinary.ini; \ + \ # set recommended PHP.ini settings -# see https://docs.nextcloud.com/server/stable/admin_manual/configuration_server/server_tuning.html#enable-php-opcache -RUN { \ - echo 'opcache.interned_strings_buffer=32'; \ +# see https://docs.nextcloud.com/server/stable/admin_manual/installation/server_tuning.html#enable-php-opcache and below + { \ + echo 'opcache.max_accelerated_files=10000'; \ + echo 'opcache.memory_consumption=256'; \ + echo 'opcache.interned_strings_buffer=64'; \ echo 'opcache.save_comments=1'; \ echo 'opcache.revalidate_freq=60'; \ + echo 'opcache.jit=1255'; \ + echo 'opcache.jit_buffer_size=8M'; \ } > /usr/local/etc/php/conf.d/opcache-recommended.ini; \ \ - echo 'apc.enable_cli=1' >> /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini; \ + { \ + echo 'apc.enable_cli=1'; \ + echo 'apc.shm_size=64M'; \ + } >> /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini; \ \ { \ echo 'memory_limit=${PHP_MEMORY_LIMIT}'; \ echo 'upload_max_filesize=${PHP_UPLOAD_LIMIT}'; \ echo 'post_max_size=${PHP_UPLOAD_LIMIT}'; \ echo 'max_execution_time=${PHP_MAX_TIME}'; \ - echo 'max_input_time=${PHP_MAX_TIME}'; \ + echo 'max_input_time=-1'; \ + echo 'default_socket_timeout=${PHP_MAX_TIME}'; \ } > /usr/local/etc/php/conf.d/nextcloud.ini; \ \ - mkdir /var/www/data; \ + { \ + echo 'session.save_handler = redis'; \ + echo 'session.save_path = "tcp://${REDIS_HOST}:${REDIS_PORT}?database=${REDIS_DB_INDEX}${REDIS_USER_AUTH}&auth[]=${REDIS_HOST_PASSWORD}"'; \ + echo 'redis.session.locking_enabled = 1'; \ + echo 'redis.session.lock_retries = -1'; \ + echo 'redis.session.lock_wait_time = 10000'; \ + echo 'session.gc_maxlifetime = 86400'; \ + } > /usr/local/etc/php/conf.d/redis-session.ini; \ + \ + mkdir -p /var/www/data; \ chown -R www-data:root /var/www; \ - chmod -R g=u /var/www - -VOLUME /var/www/html - -ENV NEXTCLOUD_VERSION 24.0.4 - -RUN set -ex; \ + chmod -R g=u /var/www; \ + \ +# Download Nextcloud archive start # Do not remove or change this line! apk add --no-cache --virtual .fetch-deps \ bzip2 \ gnupg \ ; \ \ curl -fsSL -o nextcloud.tar.bz2 \ - "https://download.nextcloud.com/server/releases/nextcloud-${NEXTCLOUD_VERSION}.tar.bz2"; \ + "https://github.com/nextcloud-releases/server/releases/download/v${NEXTCLOUD_VERSION}/nextcloud-${NEXTCLOUD_VERSION}.tar.bz2"; \ curl -fsSL -o nextcloud.tar.bz2.asc \ "https://download.nextcloud.com/server/releases/nextcloud-${NEXTCLOUD_VERSION}.tar.bz2.asc"; \ export GNUPGHOME="$(mktemp -d)"; \ @@ -126,32 +167,22 @@ RUN set -ex; \ tar -xjf nextcloud.tar.bz2 -C /usr/src/; \ gpgconf --kill all; \ rm nextcloud.tar.bz2.asc nextcloud.tar.bz2; \ - rm -rf "$GNUPGHOME" /usr/src/nextcloud/updater; \ mkdir -p /usr/src/nextcloud/data; \ mkdir -p /usr/src/nextcloud/custom_apps; \ chmod +x /usr/src/nextcloud/occ; \ - apk del .fetch-deps - -COPY *.sh upgrade.exclude / -COPY config/* /usr/src/nextcloud/config/ - -ENTRYPOINT ["/entrypoint.sh"] -CMD ["php-fpm"] - -# Template from https://github.com/nextcloud/docker/blob/master/.examples/dockerfiles/full/fpm-alpine/Dockerfile - -RUN set -ex; \ + mkdir -p /usr/src/nextcloud/config; \ + apk del .fetch-deps; \ +# Download Nextcloud archive end # Do not remove or change this line! + mv /*.php /usr/src/nextcloud/config/; \ \ +# Template from https://github.com/nextcloud/docker/blob/master/.examples/dockerfiles/full/fpm-alpine/Dockerfile apk add --no-cache \ ffmpeg \ - imagemagick \ procps \ samba-client \ supervisor \ # libreoffice \ - ; - -RUN set -ex; \ + ; \ \ apk add --no-cache --virtual .build-deps \ $PHPIZE_DEPS \ @@ -160,12 +191,15 @@ RUN set -ex; \ openssl-dev \ samba-dev \ bzip2-dev \ + libpq-dev \ ; \ \ docker-php-ext-configure imap --with-kerberos --with-imap-ssl; \ docker-php-ext-install \ bz2 \ imap \ + pgsql \ + ftp \ ; \ pecl install smbclient; \ docker-php-ext-enable smbclient; \ @@ -176,22 +210,15 @@ RUN set -ex; \ | sort -u \ | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ )"; \ - apk add --virtual .nextcloud-phpext-rundeps $runDeps; \ - apk del .build-deps - -RUN mkdir -p \ + apk add --no-cache --virtual .nextcloud-phpext-rundeps $runDeps; \ + apk del .build-deps; \ + \ + mkdir -p \ /var/log/supervisord \ /var/run/supervisord \ -; - -COPY supervisord.conf / - -ENV NEXTCLOUD_UPDATE=1 - -CMD ["/usr/bin/supervisord", "-c", "/supervisord.conf"] - -# Custom: -RUN set -ex; \ + ; \ + chmod 777 -R /var/log/supervisord; \ + chmod 777 -R /var/run/supervisord; \ \ apk add --no-cache \ bash \ @@ -201,55 +228,41 @@ RUN set -ex; \ git \ postgresql-client \ tzdata \ - mawk \ - ; \ - rm -rf /var/lib/apt/lists/* - -RUN set -ex; \ + sudo \ + grep \ + nodejs \ + bind-tools \ + imagemagick \ + imagemagick-svg \ + imagemagick-heic \ + imagemagick-tiff \ + coreutils; \ + \ grep -q '^pm = dynamic' /usr/local/etc/php-fpm.d/www.conf; \ sed -i 's/^pm = dynamic/pm = ondemand/' /usr/local/etc/php-fpm.d/www.conf; \ - sed -i 's/^pm.max_children =.*/pm.max_children = 80/' /usr/local/etc/php-fpm.d/www.conf; \ - sed -i 's/^pm.start_servers =.*/pm.start_servers = 2/' /usr/local/etc/php-fpm.d/www.conf; \ - sed -i 's/^pm.min_spare_servers =.*/pm.min_spare_servers = 1/' /usr/local/etc/php-fpm.d/www.conf; \ - sed -i 's/^pm.max_spare_servers =.*/pm.max_spare_servers = 3/' /usr/local/etc/php-fpm.d/www.conf - -RUN set -ex; \ - rm -rf /tmp/nextcloud-aio && \ - mkdir -p /tmp/nextcloud-aio && \ - cd /tmp/nextcloud-aio && \ - git clone https://github.com/nextcloud-releases/all-in-one.git --depth 1 .; \ - mkdir -p /usr/src/nextcloud/apps/nextcloud-aio; \ - cp -r ./app/* /usr/src/nextcloud/apps/nextcloud-aio/ - -RUN set -ex; \ +# Sync this with max db connections and MaxRequestWorkers +# We don't actually expect so many children but don't want to limit it artificially because people will report issues otherwise. +# Also children will usually be terminated again after the process is done due to the ondemand setting + sed -i 's/^pm.max_children =.*/pm.max_children = 5000/' /usr/local/etc/php-fpm.d/www.conf; \ + sed -i 's|access.log = /proc/self/fd/2|access.log = /proc/self/fd/1|' /usr/local/etc/php-fpm.d/docker.conf; \ + \ + echo "[ -n \"\$TERM\" ] && [ -f /root.motd ] && cat /root.motd" >> /root/.bashrc; \ + \ chown www-data:root -R /usr/src && \ - chown www-data:root -R /usr/local/etc/php/conf.d && \ - chown www-data:root -R /usr/local/etc/php-fpm.d && \ - chown www-data:root -R /var/log/supervisord/ && \ - chown www-data:root -R /var/run/supervisord/ && \ - rm -r /usr/src/nextcloud/apps/updatenotification + chmod 777 -R /usr/local/etc/php/conf.d && \ + chmod 777 -R /usr/local/etc/php-fpm.d && \ + chmod -R 777 /tmp; \ + chmod -R 777 /etc/openldap; \ + \ + mkdir -p /nc-updater; \ + chmod -R 777 /nc-updater -COPY start.sh / -COPY notify.sh / -RUN set -ex; \ - chmod +x /start.sh && \ - chmod +r /supervisord.conf && \ - chmod +x /entrypoint.sh && \ - chmod +r /upgrade.exclude && \ - chmod +x /cron.sh && \ - chmod +x /notify.sh && \ - chmod +x /activate-collabora.sh - -RUN set -ex; \ - mkdir /mnt/ncdata; \ - chown www-data:www-data /mnt/ncdata; - -VOLUME /mnt/ncdata - -# Give root a random password -RUN echo "root:$(openssl rand -base64 12)" | chpasswd - -USER www-data +# hadolint ignore=DL3002 +USER root ENTRYPOINT ["/start.sh"] +CMD ["/usr/bin/supervisord", "-c", "/supervisord.conf"] -HEALTHCHECK CMD (nc -z localhost 9000 && curl -skI localhost:7867) || exit 1 \ No newline at end of file +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/nextcloud/README.md b/Containers/nextcloud/README.md new file mode 100644 index 00000000..574afd03 --- /dev/null +++ b/Containers/nextcloud/README.md @@ -0,0 +1,35 @@ +# Nextcloud All-in-One ``nextcloud`` Container + +This folder contains the OCI/Docker container definition, along with associated resources and configuration files, for building the `nextcloud` container as part of the [Nextcloud All-in-One](https://github.com/nextcloud/all-in-one) project. This container hosts PHP and the Nextcloud Server application. + +## Overview + +The Nextcloud container provides the core Nextcloud application environment, including the necessary dependencies and configuration for seamless integration into the All-in-One stack. The container hosts: + +- The PHP SAPI/backend (php-fpm) +- Nextcloud background jobs and scheduled tasks, which are handled via cron +- Miscellaneous minor support services specific to AIO's Nextcloud deployment (health and exec) + +## Contents + +- **Dockerfile**: Instructions for building the Nextcloud container image. +- **Entrypoint script**: The `start.sh` script is used for container initialization and runtime configuration before starting supervisord. +- **Nextcloud configuration files**: Specific to running in a containerized setting and/or within AIO. +- **Supervisor**: The `supervisord.conf` file defines the long-running services hosted within the container (php-fpm, cron, etc.). + +## Usage + +This container is intended to be used as part of the All-in-One deployment and is not meant to be used on its own. Among other requirements, it needs a web server container (which AIO provides in a dedicated Apache container). It is designed to be orchestrated by the [All-in-One mastercontainer](https://github.com/nextcloud/all-in-one/tree/main/Containers/mastercontainer) or used within an [AIO Manual Installation](https://github.com/nextcloud/all-in-one/tree/main/manual-install) or [AIO Helm chart](https://github.com/nextcloud/all-in-one/tree/main/nextcloud-aio-helm-chart). + +## Documentation + +- [Nextcloud All-in-One Documentation](https://github.com/nextcloud/all-in-one#readme) +- [Nextcloud Documentation](https://docs.nextcloud.com/) + +## Contributing + +Contributions are welcome! Please follow the Nextcloud project's guidelines and submit pull requests or issues via the main repository. + +## License + +This folder and its contents are licensed under the [GNU AGPLv3](https://www.gnu.org/licenses/agpl-3.0.html), in line with the rest of Nextcloud All-in-One. diff --git a/Containers/nextcloud/activate-collabora.sh b/Containers/nextcloud/activate-collabora.sh deleted file mode 100644 index 0c10ea02..00000000 --- a/Containers/nextcloud/activate-collabora.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -if [ "$COLLABORA_ENABLED" != yes ]; then - # Basically sleep for forever if collabora is not enabled - sleep inf -fi -while ! nc -z "$NC_DOMAIN" 443; do - sleep 5 -done -sleep 10 -echo "Activating collabora config..." -php /var/www/html/occ richdocuments:activate-config -sleep inf diff --git a/Containers/nextcloud/config/aio.config.php b/Containers/nextcloud/config/aio.config.php new file mode 100644 index 00000000..7c80b6ba --- /dev/null +++ b/Containers/nextcloud/config/aio.config.php @@ -0,0 +1,5 @@ + true, + 'one-click-instance.user-limit' => 100, +); diff --git a/Containers/nextcloud/config/apps.config.php b/Containers/nextcloud/config/apps.config.php index 4c37f72a..99bf5e40 100644 --- a/Containers/nextcloud/config/apps.config.php +++ b/Containers/nextcloud/config/apps.config.php @@ -2,14 +2,20 @@ $CONFIG = array ( 'apps_paths' => array ( 0 => array ( - 'path' => OC::$SERVERROOT.'/apps', + 'path' => '/var/www/html/apps', 'url' => '/apps', 'writable' => false, ), 1 => array ( - 'path' => OC::$SERVERROOT.'/custom_apps', + 'path' => '/var/www/html/custom_apps', 'url' => '/custom_apps', 'writable' => true, ), ), ); +if (getenv('APPS_ALLOWLIST')) { + $CONFIG['appsallowlist'] = explode(" ", getenv('APPS_ALLOWLIST')); +} +if (getenv('NEXTCLOUD_APP_STORE_URL')) { + $CONFIG['appstoreurl'] = getenv('NEXTCLOUD_APP_STORE_URL'); +} diff --git a/Containers/nextcloud/config/certificates-bundle.config.php b/Containers/nextcloud/config/certificates-bundle.config.php new file mode 100644 index 00000000..cc05b06a --- /dev/null +++ b/Containers/nextcloud/config/certificates-bundle.config.php @@ -0,0 +1,5 @@ + array( + 'mode' => 'verify-ca', + 'rootcert' => '/var/www/html/data/certificates/ca-bundle.crt', + ), + ); +} +if (getenv('NEXTCLOUD_TRUSTED_CERTIFICATES_MYSQL')) { + $CONFIG = array( + 'dbdriveroptions' => array( + PDO::MYSQL_ATTR_SSL_CA => '/var/www/html/data/certificates/ca-bundle.crt', + ), + ); +} + diff --git a/Containers/nextcloud/config/proxy.config.php b/Containers/nextcloud/config/proxy.config.php new file mode 100644 index 00000000..c283f86e --- /dev/null +++ b/Containers/nextcloud/config/proxy.config.php @@ -0,0 +1,13 @@ + array( 'class' => '\OC\Files\ObjectStore\S3', 'arguments' => array( + 'multibucket' => $multibucket === 'true', + 'num_buckets' => (int)getenv('OBJECTSTORE_S3_NUM_BUCKETS') ?: 64, 'bucket' => getenv('OBJECTSTORE_S3_BUCKET'), 'key' => getenv('OBJECTSTORE_S3_KEY') ?: '', 'secret' => getenv('OBJECTSTORE_S3_SECRET') ?: '', 'region' => getenv('OBJECTSTORE_S3_REGION') ?: '', 'hostname' => getenv('OBJECTSTORE_S3_HOST') ?: '', 'port' => getenv('OBJECTSTORE_S3_PORT') ?: '', + 'storageClass' => getenv('OBJECTSTORE_S3_STORAGE_CLASS') ?: '', 'objectPrefix' => getenv("OBJECTSTORE_S3_OBJECT_PREFIX") ? getenv("OBJECTSTORE_S3_OBJECT_PREFIX") : "urn:oid:", - 'autocreate' => (strtolower($autocreate) === 'false' || $autocreate == false) ? false : true, - 'use_ssl' => (strtolower($use_ssl) === 'false' || $use_ssl == false) ? false : true, + 'autocreate' => strtolower($autocreate) !== 'false', + 'use_ssl' => strtolower($use_ssl) !== 'false', // required for some non Amazon S3 implementations - 'use_path_style' => $use_path == true && strtolower($use_path) !== 'false', + 'use_path_style' => strtolower($use_path) === 'true', // required for older protocol versions - 'legacy_auth' => $use_legacyauth == true && strtolower($use_legacyauth) !== 'false' + 'legacy_auth' => strtolower($use_legacyauth) === 'true', + 'use_nextcloud_bundle' => 1, ) ) ); -} + + $sse_c_key = getenv('OBJECTSTORE_S3_SSE_C_KEY'); + if ($sse_c_key) { + $CONFIG['objectstore']['arguments']['sse_c_key'] = $sse_c_key; + } +} diff --git a/Containers/nextcloud/config/smtp.config.php b/Containers/nextcloud/config/smtp.config.php new file mode 100644 index 00000000..b57f9b68 --- /dev/null +++ b/Containers/nextcloud/config/smtp.config.php @@ -0,0 +1,31 @@ + 'smtp', + 'mail_smtphost' => getenv('SMTP_HOST'), + 'mail_smtpport' => getenv('SMTP_PORT') ?: (getenv('SMTP_SECURE') ? 465 : 25), + 'mail_smtpsecure' => getenv('SMTP_SECURE') ?: '', + 'mail_smtpauth' => getenv('SMTP_NAME') && getenv('SMTP_PASSWORD'), + 'mail_smtpauthtype' => getenv('SMTP_AUTHTYPE') ?: 'LOGIN', + 'mail_smtpname' => getenv('SMTP_NAME') ?: '', + 'mail_from_address' => getenv('MAIL_FROM_ADDRESS'), + 'mail_domain' => getenv('MAIL_DOMAIN'), + ); + + if (getenv('SMTP_PASSWORD')) { + $CONFIG['mail_smtppassword'] = getenv('SMTP_PASSWORD'); + } else { + $CONFIG['mail_smtppassword'] = ''; + } +} + +if (getenv('NEXTCLOUD_TRUSTED_CERTIFICATES_MAILER')) { + $CONFIG = array( + 'mail_smtpstreamoptions' => array( + 'ssl' => array( + 'verify_peer_name' => false, + 'cafile' => '/var/www/html/data/certificates/ca-bundle.crt', + ) + ) + ); +} diff --git a/Containers/nextcloud/cron.sh b/Containers/nextcloud/cron.sh index 0fe5f589..0b888279 100644 --- a/Containers/nextcloud/cron.sh +++ b/Containers/nextcloud/cron.sh @@ -1,7 +1,18 @@ #!/bin/bash -set -eu +wait_for_cron() { + set -x + while [ -n "$(pgrep -f /var/www/html/cron.php)" ]; do + echo "Waiting for cron to stop..." + sleep 5 + done + echo "Cronjob successfully exited." + exit +} + +trap wait_for_cron SIGINT SIGTERM while true; do php -f /var/www/html/cron.php & - sleep 5m + sleep 5m & + wait $! done diff --git a/Containers/nextcloud/entrypoint.sh b/Containers/nextcloud/entrypoint.sh index afa6bb4b..d4b4f253 100644 --- a/Containers/nextcloud/entrypoint.sh +++ b/Containers/nextcloud/entrypoint.sh @@ -10,32 +10,107 @@ directory_empty() { [ -z "$(ls -A "$1/")" ] } -echo "Configuring Redis as session handler..." -cat << REDIS_CONF > /usr/local/etc/php/conf.d/redis-session.ini -session.save_handler = redis -session.save_path = "tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}?auth=${REDIS_HOST_PASSWORD}" -redis.session.locking_enabled = 1 -redis.session.lock_retries = -1 -# redis.session.lock_wait_time is specified in microseconds. -# Wait 10ms before retrying the lock rather than the default 2ms. -redis.session.lock_wait_time = 10000 -REDIS_CONF +run_upgrade_if_needed_due_to_app_update() { + if php /var/www/html/occ status | grep maintenance | grep -q true; then + php /var/www/html/occ maintenance:mode --off + fi + if php /var/www/html/occ status | grep needsDbUpgrade | grep -q true; then + php /var/www/html/occ upgrade + php /var/www/html/occ app:enable nextcloud-aio --force + fi +} -echo "Setting php max children..." -MEMORY=$(mawk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo) -PHP_MAX_CHILDREN=$((MEMORY/50)) -if [ -n "$PHP_MAX_CHILDREN" ]; then - sed -i "s/^pm.max_children =.*/pm.max_children = $PHP_MAX_CHILDREN/" /usr/local/etc/php-fpm.d/www.conf +# Create cert bundle +if env | grep -q NEXTCLOUD_TRUSTED_CERTIFICATES_; then + + # Enable debug mode + set -x + + # Default vars + CERTIFICATES_ROOT_DIR="/var/www/html/data/certificates" + CERTIFICATE_BUNDLE="/var/www/html/data/certificates/ca-bundle.crt" + + # Remove old root certs and recreate them with current ones + rm -rf "$CERTIFICATES_ROOT_DIR" + mkdir -p "$CERTIFICATES_ROOT_DIR" + + # Retrieve default root cert bundle + if ! [ -f "$SOURCE_LOCATION/resources/config/ca-bundle.crt" ]; then + echo "Root ca-bundle not found. Only concattening configured NEXTCLOUD_TRUSTED_CERTIFICATES files!" + # Recreate cert file + touch "$CERTIFICATE_BUNDLE" + else + # Write default bundle to the target ca file + cat "$SOURCE_LOCATION/resources/config/ca-bundle.crt" > "$CERTIFICATE_BUNDLE" + fi + + # Iterate through certs + TRUSTED_CERTIFICATES="$(env | grep NEXTCLOUD_TRUSTED_CERTIFICATES_ | grep -oP '^[A-Z_a-z0-9]+')" + mapfile -t TRUSTED_CERTIFICATES <<< "$TRUSTED_CERTIFICATES" + for certificate in "${TRUSTED_CERTIFICATES[@]}"; do + + # Create new line + echo "" >> "$CERTIFICATE_BUNDLE" + + # Check if variable is an actual cert + if echo "${!certificate}" | grep -q "BEGIN CERTIFICATE" && echo "${!certificate}" | grep -q "END CERTIFICATE"; then + # Write out cert to bundle + echo "${!certificate}" >> "$CERTIFICATE_BUNDLE" + fi + + # Create file in cert dir for extra logic in other places + if ! [ -f "$CERTIFICATES_ROOT_DIR/$CERTIFICATE_NAME" ]; then + touch "$CERTIFICATES_ROOT_DIR/$CERTIFICATE_NAME" + fi + + done + + # Backwards compatibility with older instances + if [ -f "/var/www/html/config/postgres.config.php" ]; then + sed -i "s|/var/www/html/data/certificates/POSTGRES|/var/www/html/data/certificates/ca-bundle.crt|" /var/www/html/config/postgres.config.php + sed -i "s|/var/www/html/data/certificates/MYSQL|/var/www/html/data/certificates/ca-bundle.crt|" /var/www/html/config/postgres.config.php + fi + + # Print out bundle one last time + cat "$CERTIFICATE_BUNDLE" + + # Disable debug mode + set +x fi +# Adjust DATABASE_TYPE to by Nextcloud supported value +if [ "$DATABASE_TYPE" = postgres ]; then + export DATABASE_TYPE=pgsql +fi + +# Only start container if Redis is accessible +# shellcheck disable=SC2153 +while ! nc -z "$REDIS_HOST" "$REDIS_PORT"; do + echo "Waiting for Redis to start..." + sleep 5 +done + # Check permissions in ncdata -touch "/mnt/ncdata/this-is-a-test-file" -if ! [ -f "/mnt/ncdata/this-is-a-test-file" ]; then - echo "The www-data user doesn't seem to have access rights in /mnt/ncdata. -Did you maybe change the datadir and did forget to apply the correct permissions?" +test_file="$NEXTCLOUD_DATA_DIR/this-is-a-test-file" +touch "$test_file" +if ! [ -f "$test_file" ]; then + echo "The www-data user does not appear to have access rights to the data directory." + echo "It is possible that the files are on a filesystem that does not support standard Linux permissions," + echo "or the permissions simply need to be adjusted. Please change the permissions as described below." + echo "Current permissions are:" + stat -c "%u:%g %a" "$NEXTCLOUD_DATA_DIR" + echo "(userID:groupID permissions)" + echo "They should be:" + echo "33:0 750" + echo "(userID:groupID permissions)" + echo "Also, ensure that all parent directories on the host of your chosen data directory are publicly readable." + echo "For example: sudo chmod +r /mnt (adjust this command as needed)." + echo "If you want to use a FUSE mount as the data directory, add 'allow_other' as an additional mount option." + echo "For SMB/CIFS mounts as the data directory, see:" + echo " https://github.com/nextcloud/all-in-one#can-i-use-a-cifssmb-share-as-nextclouds-datadir" exit 1 fi -rm "/mnt/ncdata/this-is-a-test-file" +rm -f "$test_file" if [ -f /var/www/html/version.php ]; then # shellcheck disable=SC2016 @@ -43,9 +118,9 @@ if [ -f /var/www/html/version.php ]; then else installed_version="0.0.0.0" fi -if [ -f "/usr/src/nextcloud/version.php" ]; then +if [ -f "$SOURCE_LOCATION/version.php" ]; then # shellcheck disable=SC2016 - image_version="$(php -r 'require "/usr/src/nextcloud/version.php"; echo implode(".", $OC_Version);')" + image_version="$(php -r "require '$SOURCE_LOCATION/version.php'; echo implode('.', \$OC_Version);")" else image_version="$installed_version" fi @@ -57,76 +132,128 @@ fi # Don't start the container if Nextcloud is not compatible with the PHP version if [ -f "/var/www/html/lib/versioncheck.php" ] && ! php /var/www/html/lib/versioncheck.php; then - echo "It seems like your installed Nextcloud is not compatible with the by the container provided PHP version." - echo "This most likely happened because you tried to restore an old Nextcloud version from backup that is not compatible with the PHP version that comes with the container." - echo "Please try to restore a more recent backup which contains a Nextcloud version that is compatible with the PHP version that comes with the container." - echo "If you do not have a more recent backup, feel free to have a look at this documentation: https://github.com/nextcloud/all-in-one/blob/main/manual-upgrade.md" + echo "Your installed Nextcloud version is not compatible with the PHP version provided by this image." + echo "This typically occurs when you restore an older Nextcloud backup that does not support the" + echo "PHP version included in this image." + echo "Please restore a more recent backup that includes a compatible Nextcloud version." + echo "If you do not have a more recent backup, refer to the manual upgrade documentation:" + echo " https://github.com/nextcloud/all-in-one/blob/main/manual-upgrade.md" exit 1 fi # Do not start the container if the last update failed -if [ -f "/mnt/ncdata/update.failed" ]; then +if [ -f "$NEXTCLOUD_DATA_DIR/update.failed" ]; then echo "The last Nextcloud update failed." - echo "Please restore from backup and try again!" - echo "If you do not have a backup in place, you can simply delete the update.failed file in the datadir which will allow the container to start again." + echo "Please restore from a backup and try again." + echo "If you do not have a backup, you can delete the update.failed file in the data directory" + echo "to allow the container to start again." + exit 1 +fi + +# Do not start the container if the install failed +if [ -f "$NEXTCLOUD_DATA_DIR/install.failed" ]; then + echo "The initial Nextcloud installation failed." + echo "For more information about what went wrong, check the logs above." + echo "Please reset AIO properly and try again." + echo "See:" + echo " https://github.com/nextcloud/all-in-one#how-to-properly-reset-the-instance" exit 1 fi # Skip any update if Nextcloud was just restored -if ! [ -f "/mnt/ncdata/skip.update" ]; then +if ! [ -f "$NEXTCLOUD_DATA_DIR/skip.update" ]; then if version_greater "$image_version" "$installed_version"; then # Check if it skips a major version INSTALLED_MAJOR="${installed_version%%.*}" IMAGE_MAJOR="${image_version%%.*}" + + if [ "$installed_version" != "0.0.0.0" ]; then + # Write output to logfile. + exec > >(tee -i "/var/www/html/data/update.log") + exec 2>&1 + fi + if [ "$installed_version" != "0.0.0.0" ] && [ "$((IMAGE_MAJOR - INSTALLED_MAJOR))" -gt 1 ]; then +# Do not skip major versions placeholder # Do not remove or change this line! +# Do not skip major versions start # Do not remove or change this line! set -ex NEXT_MAJOR="$((INSTALLED_MAJOR + 1))" curl -fsSL -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-${NEXT_MAJOR}.tar.bz2" curl -fsSL -o nextcloud.tar.bz2.asc "https://download.nextcloud.com/server/releases/latest-${NEXT_MAJOR}.tar.bz2.asc" GNUPGHOME="$(mktemp -d)" export GNUPGHOME - # gpg key from https://nextcloud.com/nextcloud.asc - gpg --batch --keyserver keyserver.ubuntu.com --recv-keys 28806A878AE423A28372792ED75899B9A724937A + if ! gpg --batch --keyserver keyserver.ubuntu.com --recv-keys 28806A878AE423A28372792ED75899B9A724937A; then + if ! gpg --batch --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 28806A878AE423A28372792ED75899B9A724937A; then + curl -sSL https://nextcloud.com/nextcloud.asc | gpg --import + fi + fi gpg --batch --verify nextcloud.tar.bz2.asc nextcloud.tar.bz2 mkdir -p /usr/src/tmp tar -xjf nextcloud.tar.bz2 -C /usr/src/tmp/ gpgconf --kill all rm nextcloud.tar.bz2.asc nextcloud.tar.bz2 - rm -rf "$GNUPGHOME" /usr/src/tmp/nextcloud/updater mkdir -p /usr/src/tmp/nextcloud/data mkdir -p /usr/src/tmp/nextcloud/custom_apps chmod +x /usr/src/tmp/nextcloud/occ - cp /usr/src/nextcloud/config/* /usr/src/tmp/nextcloud/config/ + cp -r "$SOURCE_LOCATION"/config/* /usr/src/tmp/nextcloud/config/ mkdir -p /usr/src/tmp/nextcloud/apps/nextcloud-aio - cp /usr/src/nextcloud/apps/nextcloud-aio/* /usr/src/tmp/nextcloud/apps/nextcloud-aio/ - mv /usr/src/nextcloud /usr/src/temp-nextcloud - mv /usr/src/tmp/nextcloud /usr/src/nextcloud + cp -r "$SOURCE_LOCATION"/apps/nextcloud-aio/* /usr/src/tmp/nextcloud/apps/nextcloud-aio/ + mv "$SOURCE_LOCATION" /usr/src/temp-nextcloud + mv /usr/src/tmp/nextcloud "$SOURCE_LOCATION" rm -r /usr/src/tmp rm -r /usr/src/temp-nextcloud # shellcheck disable=SC2016 - image_version="$(php -r 'require "/usr/src/nextcloud/version.php"; echo implode(".", $OC_Version);')" + image_version="$(php -r "require '$SOURCE_LOCATION/version.php'; echo implode('.', \$OC_Version);")" IMAGE_MAJOR="${image_version%%.*}" set +ex +# Do not skip major versions end # Do not remove or change this line! fi if [ "$installed_version" != "0.0.0.0" ]; then +# Check connection to appstore start # Do not remove or change this line! while true; do - echo -e "Checking connection to appstore" - CURL_STATUS="$(curl -LI "https://apps.nextcloud.com/" -o /dev/null -w '%{http_code}\n' -s)" + echo -e "Checking connection to the app store..." + APPSTORE_URL="https://apps.nextcloud.com/api/v1" + if grep -q appstoreurl /var/www/html/config/config.php; then + set -x + APPSTORE_URL="$(grep appstoreurl /var/www/html/config/config.php | grep -oP 'https://.*v[0-9]+')" + set +x + fi + # Default appstoreurl parameter in config.php defaults to 'https://apps.nextcloud.com/api/v1' so we check for the apps.json file stored in there + CURL_STATUS="$(curl -LI "$APPSTORE_URL"/apps.json -o /dev/null -w '%{http_code}\n' -s)" if [[ "$CURL_STATUS" = "200" ]] then - echo "Appstore is reachable" + echo "App store is reachable." break else - echo "Curl didn't produce a 200 status, is appstore reachable?" + echo "Curl did not return a 200 status. Is the app store reachable?" sleep 5 fi done +# Check connection to appstore end # Do not remove or change this line! + + run_upgrade_if_needed_due_to_app_update php /var/www/html/occ maintenance:mode --off - echo "Getting and backing up the status of apps for later, this might take a while..." - php /var/www/html/occ app:list | sed -n "/Enabled:/,/Disabled:/p" > /tmp/list_before + echo "Getting and backing up the status of apps for later; this might take a while..." + NC_APPS="$(find /var/www/html/custom_apps/ -type d -maxdepth 1 -mindepth 1 | sed 's|/var/www/html/custom_apps/||g')" + if [ -z "$NC_APPS" ]; then + echo "No apps detected. Aborting export of app status..." + APPSTORAGE="no-export-done" + else + mapfile -t NC_APPS_ARRAY <<< "$NC_APPS" + declare -Ag APPSTORAGE + echo "Disabling apps before the update to make the update procedure safer. This can take a while..." + for app in "${NC_APPS_ARRAY[@]}"; do + if APPSTORAGE[$app]="$(php /var/www/html/occ config:app:get "$app" enabled)"; then + php /var/www/html/occ app:disable "$app" + else + APPSTORAGE[$app]="" + echo "Not disabling $app because the occ command to get its enabled state failed." + fi + done + fi if [ "$((IMAGE_MAJOR - INSTALLED_MAJOR))" -eq 1 ]; then php /var/www/html/occ config:system:delete app_install_overwrite @@ -134,68 +261,195 @@ if ! [ -f "/mnt/ncdata/skip.update" ]; then php /var/www/html/occ app:update --all - # Fix removing the updatenotification for old instances - if [ -d "/var/www/html/apps/updatenotification" ]; then - php /var/www/html/occ app:disable updatenotification - fi + run_upgrade_if_needed_due_to_app_update fi - echo "Initializing nextcloud $image_version ..." - rsync -rlD --delete --exclude-from=/upgrade.exclude /usr/src/nextcloud/ /var/www/html/ + echo "Initializing Nextcloud $image_version ..." + # Copy over initial data from Nextcloud archive + rsync -rlD --delete \ + --exclude-from=/upgrade.exclude \ + "$SOURCE_LOCATION/" \ + /var/www/html/ + + # Copy custom_apps from Nextcloud archive + if ! directory_empty "$SOURCE_LOCATION/custom_apps"; then + set -x + for app in "$SOURCE_LOCATION/custom_apps"/*; do + app_id="$(basename "$app")" + mkdir -p "/var/www/html/custom_apps/$app_id" + rsync -rlD --delete \ + --include "/$app_id/" \ + --exclude '/*' \ + "$SOURCE_LOCATION/custom_apps/" \ + /var/www/html/custom_apps/ + done + set +x + fi + + # Copy these from Nextcloud archive if they don't exist yet (i.e. new install) for dir in config data custom_apps themes; do if [ ! -d "/var/www/html/$dir" ] || directory_empty "/var/www/html/$dir"; then - rsync -rlD --include "/$dir/" --exclude '/*' /usr/src/nextcloud/ /var/www/html/ + rsync -rlD \ + --include "/$dir/" \ + --exclude '/*' \ + "$SOURCE_LOCATION/" \ + /var/www/html/ fi done - rsync -rlD --include '/version.php' --exclude '/*' /usr/src/nextcloud/ /var/www/html/ + + rsync -rlD --delete \ + --include '/config/' \ + --exclude '/*' \ + --exclude '/config/CAN_INSTALL' \ + --exclude '/config/config.sample.php' \ + --exclude '/config/config.php' \ + "$SOURCE_LOCATION/" \ + /var/www/html/ + + rsync -rlD \ + --include '/version.php' \ + --exclude '/*' \ + "$SOURCE_LOCATION/" \ + /var/www/html/ + echo "Initializing finished" - #install + ################ + # Fresh Install + ################ + if [ "$installed_version" = "0.0.0.0" ]; then - echo "New nextcloud instance" + echo "New Nextcloud instance." + + # Write output to logfile. + mkdir -p /var/www/html/data + exec > >(tee -i "/var/www/html/data/install.log") + exec 2>&1 INSTALL_OPTIONS=(-n --admin-user "$ADMIN_USER" --admin-pass "$ADMIN_PASSWORD") if [ -n "${NEXTCLOUD_DATA_DIR}" ]; then INSTALL_OPTIONS+=(--data-dir "$NEXTCLOUD_DATA_DIR") fi - echo "Installing with PostgreSQL database" - INSTALL_OPTIONS+=(--database pgsql --database-name "$POSTGRES_DB" --database-user "$POSTGRES_USER" --database-pass "$POSTGRES_PASSWORD" --database-host "$POSTGRES_HOST") + # Skip the default permission check (we do our own) + cat > /var/www/html/config/datadir.permission.config.php <<'EOF' + false + ); +EOF - echo "starting nextcloud installation" - max_retries=10 - try=0 - until php /var/www/html/occ maintenance:install "${INSTALL_OPTIONS[@]}" || [ "$try" -gt "$max_retries" ] - do - echo "retrying install..." - try=$((try+1)) - sleep 10s - done - if [ "$try" -gt "$max_retries" ]; then - echo "installing of nextcloud failed!" + echo "Installing with $DATABASE_TYPE database" + # Set a default value for POSTGRES_PORT + if [ -z "$POSTGRES_PORT" ]; then + POSTGRES_PORT=5432 + fi + + # Add database options to INSTALL_OPTIONS + # shellcheck disable=SC2153 + INSTALL_OPTIONS+=( + --database "$DATABASE_TYPE" + --database-name "$POSTGRES_DB" + --database-user "$POSTGRES_USER" + --database-pass "$POSTGRES_PASSWORD" + --database-host "$POSTGRES_HOST" + --database-port "$POSTGRES_PORT" + ) + + echo "Starting Nextcloud installation..." + if ! php /var/www/html/occ maintenance:install "${INSTALL_OPTIONS[@]}"; then + echo "Installation of Nextcloud failed!" + touch "$NEXTCLOUD_DATA_DIR/install.failed" exit 1 fi + # Try to force generation of appdata dir: + php /var/www/html/occ maintenance:repair + + if [ -z "$OBJECTSTORE_S3_BUCKET" ] && [ -z "$OBJECTSTORE_SWIFT_URL" ]; then + max_retries=10 + try=0 + while [ -z "$(find "$NEXTCLOUD_DATA_DIR/" -maxdepth 1 -mindepth 1 -type d -name "appdata_*")" ] && [ "$try" -lt "$max_retries" ]; do + echo "Waiting for appdata to become available..." + try=$((try+1)) + sleep 10s + done + + if [ "$try" -ge "$max_retries" ]; then + echo "Installation of Nextcloud failed!" + echo "Installation errors: $(cat /var/www/html/data/nextcloud.log)" + touch "$NEXTCLOUD_DATA_DIR/install.failed" + exit 1 + fi + fi + + # This autoconfig is not needed anymore and should be able to be overwritten by the user + rm /var/www/html/config/datadir.permission.config.php + # unset admin password unset ADMIN_PASSWORD + # Enable the updatenotification app but disable its UI and server update notifications + php /var/www/html/occ config:system:set updatechecker --type=bool --value=false + php /var/www/html/occ config:app:set updatenotification notify_groups --value="[]" + +# AIO update to latest start # Do not remove or change this line! + if [ "$INSTALL_LATEST_MAJOR" = yes ]; then + php /var/www/html/occ config:system:set updatedirectory --value="/nc-updater" + INSTALLED_AT="$(php /var/www/html/occ config:app:get core installedat)" + if [ -n "${INSTALLED_AT}" ]; then + # Set the installdat to 00 which will allow to skip staging and install the next major directly + # shellcheck disable=SC2001 + INSTALLED_AT="$(echo "${INSTALLED_AT}" | sed "s|[0-9][0-9]$|00|")" + php /var/www/html/occ config:app:set core installedat --value="${INSTALLED_AT}" + fi + php /var/www/html/updater/updater.phar --no-interaction --no-backup + if ! php /var/www/html/occ -V || php /var/www/html/occ status | grep maintenance | grep -q 'true'; then + echo "Installation of Nextcloud failed!" + touch "$NEXTCLOUD_DATA_DIR/install.failed" + exit 1 + fi + # shellcheck disable=SC2016 + installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" + INSTALLED_MAJOR="${installed_version%%.*}" + IMAGE_MAJOR="${image_version%%.*}" + # If a valid upgrade path, trigger the Nextcloud built-in Updater + if ! [ "$INSTALLED_MAJOR" -gt "$IMAGE_MAJOR" ]; then + php /var/www/html/updater/updater.phar --no-interaction --no-backup + if ! php /var/www/html/occ -V || php /var/www/html/occ status | grep maintenance | grep -q 'true'; then + echo "Installation of Nextcloud failed!" + # TODO: Add a hint here about what to do / where to look / updater.log? + touch "$NEXTCLOUD_DATA_DIR/install.failed" + exit 1 + fi + # shellcheck disable=SC2016 + installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" + fi + php /var/www/html/occ config:system:set updatechecker --type=bool --value=true + php /var/www/html/occ app:enable nextcloud-aio --force + php /var/www/html/occ db:add-missing-columns + php /var/www/html/occ db:add-missing-primary-keys + yes | php /var/www/html/occ db:convert-filecache-bigint + fi +# AIO update to latest end # Do not remove or change this line! + # Apply log settings echo "Applying default settings..." mkdir -p /var/www/html/data - php /var/www/html/occ config:system:set loglevel --value=2 - php /var/www/html/occ config:system:set log_type --value=file + php /var/www/html/occ config:system:set loglevel --value="2" --type=integer + php /var/www/html/occ config:system:set log_type --value="file" php /var/www/html/occ config:system:set logfile --value="/var/www/html/data/nextcloud.log" - php /var/www/html/occ config:system:set log_rotate_size --value="10485760" + php /var/www/html/occ config:system:set log_rotate_size --value="10485760" --type=integer php /var/www/html/occ app:enable admin_audit php /var/www/html/occ config:app:set admin_audit logfile --value="/var/www/html/data/audit.log" php /var/www/html/occ config:system:set log.condition apps 0 --value="admin_audit" # Apply preview settings echo "Applying preview settings..." - php /var/www/html/occ config:system:set preview_max_x --value="2048" - php /var/www/html/occ config:system:set preview_max_y --value="2048" - php /var/www/html/occ config:system:set jpeg_quality --value="60" + php /var/www/html/occ config:system:set preview_max_x --value="2048" --type=integer + php /var/www/html/occ config:system:set preview_max_y --value="2048" --type=integer + php /var/www/html/occ config:system:set jpeg_quality --value="60" --type=integer php /var/www/html/occ config:app:set preview jpeg_quality --value="60" php /var/www/html/occ config:system:delete enabledPreviewProviders php /var/www/html/occ config:system:set enabledPreviewProviders 1 --value="OC\\Preview\\Image" @@ -204,112 +458,228 @@ if ! [ -f "/mnt/ncdata/skip.update" ]; then php /var/www/html/occ config:system:set enabledPreviewProviders 4 --value="OC\\Preview\\TXT" php /var/www/html/occ config:system:set enabledPreviewProviders 5 --value="OC\\Preview\\OpenDocument" php /var/www/html/occ config:system:set enabledPreviewProviders 6 --value="OC\\Preview\\Movie" + php /var/www/html/occ config:system:set enabledPreviewProviders 7 --value="OC\\Preview\\Krita" php /var/www/html/occ config:system:set enable_previews --value=true --type=boolean # Apply other settings echo "Applying other settings..." + # Add missing indices after new installation because they seem to be missing on new installation + php /var/www/html/occ db:add-missing-indices php /var/www/html/occ config:system:set upgrade.disable-web --type=bool --value=true php /var/www/html/occ config:system:set mail_smtpmode --value="smtp" php /var/www/html/occ config:system:set trashbin_retention_obligation --value="auto, 30" php /var/www/html/occ config:system:set versions_retention_obligation --value="auto, 30" - php /var/www/html/occ config:system:set activity_expire_days --value="30" + php /var/www/html/occ config:system:set activity_expire_days --value="30" --type=integer php /var/www/html/occ config:system:set simpleSignUpLink.shown --type=bool --value=false php /var/www/html/occ config:system:set share_folder --value="/Shared" - # Not needed anymore with the removal of the updatenotification app: - # php /var/www/html/occ config:app:set updatenotification notify_groups --value="[]" # Install some apps by default - php /var/www/html/occ app:install twofactor_totp - php /var/www/html/occ app:install deck - php /var/www/html/occ app:install tasks - php /var/www/html/occ app:install calendar - php /var/www/html/occ app:install contacts - php /var/www/html/occ app:install apporder + if [ -n "$STARTUP_APPS" ]; then + read -ra STARTUP_APPS_ARRAY <<< "$STARTUP_APPS" + for app in "${STARTUP_APPS_ARRAY[@]}"; do + if ! echo "$app" | grep -q '^-'; then + if [ -z "$(find /var/www/html/apps /var/www/html/custom_apps -type d -maxdepth 1 -mindepth 1 -name "$app" )" ]; then + # If not shipped, install and enable the app + php /var/www/html/occ app:install "$app" + else + # If shipped, enable the app + php /var/www/html/occ app:enable "$app" + fi + else + app="${app#-}" + # Disable the app if '-' was provided in front of the appid + php /var/www/html/occ app:disable "$app" + fi + done + fi #upgrade else - touch "/mnt/ncdata/update.failed" - while [ -n "$(pgrep -f cron.php)" ] - do - echo "Waiting for Nextclouds cronjob to finish..." - sleep 5 - done - - echo "Upgrading nextcloud from $installed_version to $image_version..." + touch "$NEXTCLOUD_DATA_DIR/update.failed" + echo "Upgrading Nextcloud from $installed_version to $image_version..." + php /var/www/html/occ config:system:delete integrity.check.disabled if ! php /var/www/html/occ upgrade || ! php /var/www/html/occ -V; then echo "Upgrade failed. Please restore from backup." - bash /notify.sh "Nextcloud update to $image_version failed!" "Please restore from backup!" + bash /notify.sh "Nextcloud update to $image_version failed!" "Please restore from backup." exit 1 fi - rm "/mnt/ncdata/update.failed" - bash /notify.sh "Nextcloud update to $image_version successful!" "Feel free to inspect the Nextcloud container logs for more info." + # shellcheck disable=SC2016 + installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" - php /var/www/html/occ app:list | sed -n "/Enabled:/,/Disabled:/p" > /tmp/list_after - echo "The following apps have been disabled:" - diff /tmp/list_before /tmp/list_after | grep '<' | cut -d- -f2 | cut -d: -f1 - rm -f /tmp/list_before /tmp/list_after + rm "$NEXTCLOUD_DATA_DIR/update.failed" + bash /notify.sh "Nextcloud update to $image_version successful!" "You may inspect the Nextcloud container logs for more information." + + php /var/www/html/occ app:update --all + + run_upgrade_if_needed_due_to_app_update + + # Restore app status + if [ "${APPSTORAGE[0]}" != "no-export-done" ]; then + echo "Restoring app statuses. This may take a while..." + for app in "${!APPSTORAGE[@]}"; do + if [ -n "${APPSTORAGE[$app]}" ]; then + if [ "${APPSTORAGE[$app]}" != "no" ]; then + echo "Enabling $app..." + if ! php /var/www/html/occ app:enable "$app" >/dev/null; then + php /var/www/html/occ app:disable "$app" >/dev/null + if ! php /var/www/html/occ -V &>/dev/null; then + rm -r "/var/www/html/custom_apps/$app" + php /var/www/html/occ maintenance:mode --off + fi + run_upgrade_if_needed_due_to_app_update + echo "The $app app could not be re-enabled, probably because it is not compatible with the new Nextcloud version." + if [ "$app" = apporder ]; then + CUSTOM_HINT="The apporder app was deprecated. A possible replacement is the side_menu app, aka 'Custom menu'." + else + CUSTOM_HINT="Most likely, it is not compatible with the new Nextcloud version." + fi + bash /notify.sh "Could not re-enable the $app app after the Nextcloud update!" "$CUSTOM_HINT Feel free to review the Nextcloud update logs and force-enable the app again if you wish." + continue + fi + # Only restore the group settings, if the app was enabled (and is thus compatible with the new NC version) + if [ "${APPSTORAGE[$app]}" != "yes" ]; then + php /var/www/html/occ config:app:set "$app" enabled --value="${APPSTORAGE[$app]}" + fi + fi + fi + done + fi + + php /var/www/html/occ app:update --all + + run_upgrade_if_needed_due_to_app_update + + # Enable the updatenotification app but disable its UI and server update notifications + php /var/www/html/occ config:system:set updatechecker --type=bool --value=false + php /var/www/html/occ app:enable updatenotification + php /var/www/html/occ config:app:set updatenotification notify_groups --value="[]" # Apply optimization - echo "Doing some optimizations..." - php /var/www/html/occ maintenance:repair - php /var/www/html/occ db:add-missing-indices - php /var/www/html/occ db:add-missing-columns - php /var/www/html/occ db:add-missing-primary-keys - yes | php /var/www/html/occ db:convert-filecache-bigint - php /var/www/html/occ maintenance:mimetype:update-js - php /var/www/html/occ maintenance:mimetype:update-db + echo "Performing some optimizations..." + if [ "$NEXTCLOUD_SKIP_DATABASE_OPTIMIZATION" != yes ]; then + php /var/www/html/occ maintenance:repair --include-expensive + php /var/www/html/occ db:add-missing-indices + php /var/www/html/occ db:add-missing-columns + php /var/www/html/occ db:add-missing-primary-keys + yes | php /var/www/html/occ db:convert-filecache-bigint + else + php /var/www/html/occ maintenance:repair + fi fi fi # Performing update of all apps if daily backups are enabled, running and successful and if it is saturday if [ "$UPDATE_NEXTCLOUD_APPS" = 'yes' ] && [ "$(date +%u)" = 6 ]; then UPDATED_APPS="$(php /var/www/html/occ app:update --all)" + run_upgrade_if_needed_due_to_app_update if [ -n "$UPDATED_APPS" ]; then bash /notify.sh "Your apps just got updated!" "$UPDATED_APPS" fi fi +else + SKIP_UPDATE=1 fi -# Check if appdata is present -# If not, something broke (e.g. changing ncdatadir after aio was first started) -if [ -z "$(find "/mnt/ncdata/" -maxdepth 1 -mindepth 1 -type d -name "appdata_*")" ]; then - echo "Appdata is not present. Did you maybe change the datadir after aio was first started?" - exit 1 -fi +run_upgrade_if_needed_due_to_app_update -# Configure tempdirectory if [ -z "$OBJECTSTORE_S3_BUCKET" ] && [ -z "$OBJECTSTORE_SWIFT_URL" ]; then - mkdir -p "/mnt/ncdata/tmp/" - if ! grep -q upload_tmp_dir /usr/local/etc/php/conf.d/nextcloud.ini; then - echo "upload_tmp_dir = /mnt/ncdata/tmp/" >> /usr/local/etc/php/conf.d/nextcloud.ini + # Check if appdata is present + # If not, something broke (e.g. changing ncdatadir after aio was first started) + if [ -z "$(find "$NEXTCLOUD_DATA_DIR/" -maxdepth 1 -mindepth 1 -type d -name "appdata_*")" ]; then + echo "Appdata is not present. Did you change the datadir after the initial Nextcloud installation? This is not supported!" + echo "See https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir" + echo "If you moved the datadir to an external drive, make sure that the drive is still mounted." + echo "The following was found in the datadir:" + ls -la "$NEXTCLOUD_DATA_DIR/" + exit 1 fi - php /var/www/html/occ config:system:set tempdirectory --value="/mnt/ncdata/tmp/" + + # Delete formerly configured tempdirectory as the default is usually faster (if the datadir is on a HDD or network FS) + if [ "$(php /var/www/html/occ config:system:get tempdirectory)" = "$NEXTCLOUD_DATA_DIR/tmp/" ]; then + php /var/www/html/occ config:system:delete tempdirectory + if [ -d "$NEXTCLOUD_DATA_DIR/tmp/" ]; then + rm -r "$NEXTCLOUD_DATA_DIR/tmp/" + fi + fi + fi # Perform fingerprint update if instance was restored -if [ -f "/mnt/ncdata/fingerprint.update" ]; then +if [ -f "$NEXTCLOUD_DATA_DIR/fingerprint.update" ]; then php /var/www/html/occ maintenance:data-fingerprint - rm "/mnt/ncdata/fingerprint.update" + rm "$NEXTCLOUD_DATA_DIR/fingerprint.update" fi +# Perform preview scan if previews were excluded from restore +if [ -f "$NEXTCLOUD_DATA_DIR/trigger-preview.scan" ]; then + php /var/www/html/occ files:scan-app-data preview -vvv + rm "$NEXTCLOUD_DATA_DIR/trigger-preview.scan" +fi + +# AIO one-click settings start # Do not remove or change this line! # Apply one-click-instance settings echo "Applying one-click-instance settings..." php /var/www/html/occ config:system:set one-click-instance --value=true --type=bool php /var/www/html/occ config:system:set one-click-instance.user-limit --value=100 --type=int +php /var/www/html/occ config:system:set one-click-instance.link --value="https://nextcloud.com/all-in-one/" +# AIO one-click settings end # Do not remove or change this line! +php /var/www/html/occ app:enable support +if [ -n "$SUBSCRIPTION_KEY" ] && [ -z "$(php /var/www/html/occ config:app:get support potential_subscription_key)" ]; then + php /var/www/html/occ config:app:set support potential_subscription_key --value="$SUBSCRIPTION_KEY" + php /var/www/html/occ config:app:delete support last_check +fi +if [ -n "$NEXTCLOUD_DEFAULT_QUOTA" ]; then + if [ "$NEXTCLOUD_DEFAULT_QUOTA" = "unlimited" ]; then + php /var/www/html/occ config:app:delete files default_quota + else + php /var/www/html/occ config:app:set files default_quota --value="$NEXTCLOUD_DEFAULT_QUOTA" + fi +fi # Adjusting log files to be stored on a volume echo "Adjusting log files..." +php /var/www/html/occ config:system:set upgrade.cli-upgrade-link --value="https://github.com/nextcloud/all-in-one/discussions/2726" php /var/www/html/occ config:system:set logfile --value="/var/www/html/data/nextcloud.log" php /var/www/html/occ config:app:set admin_audit logfile --value="/var/www/html/data/audit.log" +php /var/www/html/occ config:system:set updatedirectory --value="/nc-updater" +if [ -n "$NEXTCLOUD_SKELETON_DIRECTORY" ]; then + if [ "$NEXTCLOUD_SKELETON_DIRECTORY" = "empty" ]; then + php /var/www/html/occ config:system:set skeletondirectory --value="" + else + php /var/www/html/occ config:system:set skeletondirectory --value="$NEXTCLOUD_SKELETON_DIRECTORY" + fi +fi +if [ -n "$SERVERINFO_TOKEN" ] && [ -z "$(php /var/www/html/occ config:app:get serverinfo token)" ]; then + php /var/www/html/occ config:app:set serverinfo token --value="$SERVERINFO_TOKEN" +fi +# Set maintenance window so that no warning is shown in the admin overview +if [ -z "$NEXTCLOUD_MAINTENANCE_WINDOW" ]; then + NEXTCLOUD_MAINTENANCE_WINDOW=100 +fi +php /var/www/html/occ config:system:set maintenance_window_start --type=int --value="$NEXTCLOUD_MAINTENANCE_WINDOW" # Apply network settings echo "Applying network settings..." +php /var/www/html/occ config:system:set allow_local_remote_servers --type=bool --value=true +php /var/www/html/occ config:system:set davstorage.request_timeout --value="$PHP_MAX_TIME" --type=int php /var/www/html/occ config:system:set trusted_domains 1 --value="$NC_DOMAIN" php /var/www/html/occ config:system:set overwrite.cli.url --value="https://$NC_DOMAIN/" +php /var/www/html/occ config:system:set documentation_url.server_logs --value="https://github.com/nextcloud/all-in-one/discussions/5425" php /var/www/html/occ config:system:set htaccess.RewriteBase --value="/" php /var/www/html/occ maintenance:update:htaccess +# Revert dbpersistent setting to check if it fixes too many db connections +php /var/www/html/occ config:system:set dbpersistent --value=false --type=bool + +if [ "$DISABLE_BRUTEFORCE_PROTECTION" = yes ]; then + php /var/www/html/occ config:system:set auth.bruteforce.protection.enabled --type=bool --value=false + php /var/www/html/occ config:system:set ratelimit.protection.enabled --type=bool --value=false +else + php /var/www/html/occ config:system:set auth.bruteforce.protection.enabled --type=bool --value=true + php /var/www/html/occ config:system:set ratelimit.protection.enabled --type=bool --value=true +fi + # Disallow creating local external storages when nothing was mounted if [ -z "$NEXTCLOUD_MOUNT" ]; then php /var/www/html/occ config:system:set files_external_allow_create_new_local --type=bool --value=false @@ -317,176 +687,378 @@ else php /var/www/html/occ config:system:set files_external_allow_create_new_local --type=bool --value=true fi +# AIO app start # Do not remove or change this line! # AIO app -if [ "$(php /var/www/html/occ config:app:get nextcloud-aio enabled)" = "" ]; then - php /var/www/html/occ app:enable nextcloud-aio -elif [ "$(php /var/www/html/occ config:app:get nextcloud-aio enabled)" = "no" ]; then - php /var/www/html/occ app:enable nextcloud-aio +if [ "$THIS_IS_AIO" = "true" ]; then + if [ "$(php /var/www/html/occ config:app:get nextcloud-aio enabled)" != "yes" ]; then + php /var/www/html/occ app:enable nextcloud-aio + fi +else + if [ "$(php /var/www/html/occ config:app:get nextcloud-aio enabled)" != "no" ]; then + php /var/www/html/occ app:disable nextcloud-aio + fi fi +# AIO app end # Do not remove or change this line! # Notify push if ! [ -d "/var/www/html/custom_apps/notify_push" ]; then php /var/www/html/occ app:install notify_push -elif [ "$(php /var/www/html/occ config:app:get notify_push enabled)" = "no" ]; then +elif [ "$(php /var/www/html/occ config:app:get notify_push enabled)" != "yes" ]; then php /var/www/html/occ app:enable notify_push -else +elif [ "$SKIP_UPDATE" != 1 ]; then php /var/www/html/occ app:update notify_push fi +chmod 775 -R /var/www/html/custom_apps/notify_push/bin/ php /var/www/html/occ config:system:set trusted_proxies 0 --value="127.0.0.1" php /var/www/html/occ config:system:set trusted_proxies 1 --value="::1" +if [ -n "$ADDITIONAL_TRUSTED_PROXY" ]; then + php /var/www/html/occ config:system:set trusted_proxies 2 --value="$ADDITIONAL_TRUSTED_PROXY" +fi + +# Get ipv4-address of Nextcloud +if [ -z "$NEXTCLOUD_HOST" ]; then + export NEXTCLOUD_HOST="nextcloud-aio-nextcloud" +fi +IPv4_ADDRESS="$(dig "$NEXTCLOUD_HOST" A +short +search | head -1)" +# Bring it in CIDR notation +# shellcheck disable=SC2001 +IPv4_ADDRESS="$(echo "$IPv4_ADDRESS" | sed 's|[0-9]\+$|0/16|')" +if [ -n "$IPv4_ADDRESS" ]; then + php /var/www/html/occ config:system:set trusted_proxies 10 --value="$IPv4_ADDRESS" +fi + +if [ -n "$ADDITIONAL_TRUSTED_DOMAIN" ]; then + php /var/www/html/occ config:system:set trusted_domains 2 --value="$ADDITIONAL_TRUSTED_DOMAIN" +fi php /var/www/html/occ config:app:set notify_push base_endpoint --value="https://$NC_DOMAIN/push" # Collabora if [ "$COLLABORA_ENABLED" = 'yes' ]; then + set -x + if echo "$COLLABORA_HOST" | grep -q "nextcloud-.*-collabora"; then + COLLABORA_HOST="$NC_DOMAIN" + fi + set +x + # Remove richdcoumentscode if it should be incorrectly installed + if [ -d "/var/www/html/custom_apps/richdocumentscode" ]; then + php /var/www/html/occ app:remove richdocumentscode + fi if ! [ -d "/var/www/html/custom_apps/richdocuments" ]; then php /var/www/html/occ app:install richdocuments - elif [ "$(php /var/www/html/occ config:app:get richdocuments enabled)" = "no" ]; then + elif [ "$(php /var/www/html/occ config:app:get richdocuments enabled)" != "yes" ]; then php /var/www/html/occ app:enable richdocuments - else + elif [ "$SKIP_UPDATE" != 1 ]; then php /var/www/html/occ app:update richdocuments fi - php /var/www/html/occ config:app:set richdocuments wopi_url --value="https://$NC_DOMAIN/" - # Fix https://github.com/nextcloud/all-in-one/issues/188: - php /var/www/html/occ config:system:set allow_local_remote_servers --type=bool --value=true + php /var/www/html/occ config:app:set richdocuments wopi_url --value="https://$COLLABORA_HOST/" + # Make collabora more save + COLLABORA_IPv4_ADDRESS="$(dig "$COLLABORA_HOST" A +short +search | grep '^[0-9.]\+$' | sort | head -n1)" + COLLABORA_IPv6_ADDRESS="$(dig "$COLLABORA_HOST" AAAA +short +search | grep '^[0-9a-f:]\+$' | sort | head -n1)" + COLLABORA_ALLOW_LIST="$(php /var/www/html/occ config:app:get richdocuments wopi_allowlist)" + if [ -n "$COLLABORA_IPv4_ADDRESS" ]; then + if ! echo "$COLLABORA_ALLOW_LIST" | grep -q "$COLLABORA_IPv4_ADDRESS"; then + if [ -z "$COLLABORA_ALLOW_LIST" ]; then + COLLABORA_ALLOW_LIST="$COLLABORA_IPv4_ADDRESS" + else + COLLABORA_ALLOW_LIST+=",$COLLABORA_IPv4_ADDRESS" + fi + fi + else + echo "Warning: No IPv4 address found for $COLLABORA_HOST." + fi + if [ -n "$COLLABORA_IPv6_ADDRESS" ]; then + if ! echo "$COLLABORA_ALLOW_LIST" | grep -q "$COLLABORA_IPv6_ADDRESS"; then + if [ -z "$COLLABORA_ALLOW_LIST" ]; then + COLLABORA_ALLOW_LIST="$COLLABORA_IPv6_ADDRESS" + else + COLLABORA_ALLOW_LIST+=",$COLLABORA_IPv6_ADDRESS" + fi + fi + else + echo "No IPv6 address found for $COLLABORA_HOST." + fi + if [ -n "$COLLABORA_ALLOW_LIST" ]; then + PRIVATE_IP_RANGES='127.0.0.0/8,192.168.0.0/16,172.16.0.0/12,10.0.0.0/8,100.64.0.0/10,fd00::/8,::1/128' + if ! echo "$COLLABORA_ALLOW_LIST" | grep -q "$PRIVATE_IP_RANGES"; then + COLLABORA_ALLOW_LIST+=",$PRIVATE_IP_RANGES" + fi + if [ -n "$ADDITIONAL_TRUSTED_PROXY" ]; then + if ! echo "$COLLABORA_ALLOW_LIST" | grep -q "$ADDITIONAL_TRUSTED_PROXY"; then + COLLABORA_ALLOW_LIST+=",$ADDITIONAL_TRUSTED_PROXY" + fi + fi + php /var/www/html/occ config:app:set richdocuments wopi_allowlist --value="$COLLABORA_ALLOW_LIST" + else + echo "Warning: wopi_allowlist is empty; this should not be the case!" + fi else - if [ -d "/var/www/html/custom_apps/richdocuments" ]; then + if [ "$REMOVE_DISABLED_APPS" = yes ] && [ -d "/var/www/html/custom_apps/richdocuments" ]; then php /var/www/html/occ app:remove richdocuments fi fi # OnlyOffice if [ "$ONLYOFFICE_ENABLED" = 'yes' ]; then - while ! nc -z "$ONLYOFFICE_HOST" 80; do - echo "waiting for OnlyOffice to become available..." + # Determine OnlyOffice port based on host pattern + if echo "$ONLYOFFICE_HOST" | grep -q "nextcloud-.*-onlyoffice"; then + ONLYOFFICE_PORT=80 + else + ONLYOFFICE_PORT=443 + fi + + count=0 + while ! nc -z "$ONLYOFFICE_HOST" "$ONLYOFFICE_PORT" && [ "$count" -lt 90 ]; do + echo "Waiting for OnlyOffice to become available..." + count=$((count+5)) sleep 5 done - if ! [ -d "/var/www/html/custom_apps/onlyoffice" ]; then - php /var/www/html/occ app:install onlyoffice - elif [ "$(php /var/www/html/occ config:app:get onlyoffice enabled)" = "no" ]; then - php /var/www/html/occ app:enable onlyoffice + if [ "$count" -ge 90 ]; then + bash /notify.sh "Onlyoffice did not start in time!" "Skipping initialization and disabling onlyoffice app." + php /var/www/html/occ app:disable onlyoffice else - php /var/www/html/occ app:update onlyoffice + # Install or enable OnlyOffice app as needed + if ! [ -d "/var/www/html/custom_apps/onlyoffice" ]; then + php /var/www/html/occ app:install onlyoffice + elif [ "$(php /var/www/html/occ config:app:get onlyoffice enabled)" != "yes" ]; then + php /var/www/html/occ app:enable onlyoffice + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update onlyoffice + fi + + # Set OnlyOffice configuration + php /var/www/html/occ config:system:set onlyoffice editors_check_interval --value="0" --type=integer + php /var/www/html/occ config:system:set onlyoffice jwt_secret --value="$ONLYOFFICE_SECRET" + php /var/www/html/occ config:app:set onlyoffice jwt_secret --value="$ONLYOFFICE_SECRET" + php /var/www/html/occ config:system:set onlyoffice jwt_header --value="AuthorizationJwt" + + # Adjust the OnlyOffice host if using internal pattern + if echo "$ONLYOFFICE_HOST" | grep -q "nextcloud-.*-onlyoffice"; then + ONLYOFFICE_HOST="$NC_DOMAIN/onlyoffice" + export ONLYOFFICE_HOST + fi + + php /var/www/html/occ config:app:set onlyoffice DocumentServerUrl --value="https://$ONLYOFFICE_HOST" fi - php /var/www/html/occ config:system:set onlyoffice jwt_secret --value="$ONLYOFFICE_SECRET" - php /var/www/html/occ config:system:set onlyoffice jwt_header --value="AuthorizationJwt" - php /var/www/html/occ config:app:set onlyoffice DocumentServerUrl --value="https://$NC_DOMAIN/onlyoffice" - php /var/www/html/occ config:system:set allow_local_remote_servers --type=bool --value=true else - if [ -d "/var/www/html/custom_apps/onlyoffice" ]; then + # Remove OnlyOffice app if disabled and removal is requested + if [ "$REMOVE_DISABLED_APPS" = yes ] && \ + [ -d "/var/www/html/custom_apps/onlyoffice" ] && \ + [ -n "$ONLYOFFICE_SECRET" ] && \ + [ "$(php /var/www/html/occ config:system:get onlyoffice jwt_secret)" = "$ONLYOFFICE_SECRET" ]; then php /var/www/html/occ app:remove onlyoffice fi fi # Talk if [ "$TALK_ENABLED" = 'yes' ]; then + set -x + if [ -z "$TALK_HOST" ] || echo "$TALK_HOST" | grep -q "nextcloud-.*-talk"; then + TALK_HOST="$NC_DOMAIN" + HPB_PATH="/standalone-signaling/" + fi + if [ -z "$TURN_DOMAIN" ]; then + TURN_DOMAIN="$TALK_HOST" + fi + set +x if ! [ -d "/var/www/html/custom_apps/spreed" ]; then php /var/www/html/occ app:install spreed - elif [ "$(php /var/www/html/occ config:app:get spreed enabled)" = "no" ]; then + elif [ "$(php /var/www/html/occ config:app:get spreed enabled)" != "yes" ]; then php /var/www/html/occ app:enable spreed - else + elif [ "$SKIP_UPDATE" != 1 ]; then php /var/www/html/occ app:update spreed fi - STUN_SERVERS="[\"$NC_DOMAIN:$TALK_PORT\"]" - TURN_SERVERS="[{\"server\":\"$NC_DOMAIN:$TALK_PORT\",\"secret\":\"$TURN_SECRET\",\"protocols\":\"udp,tcp\"}]" - SIGNALING_SERVERS="{\"servers\":[{\"server\":\"https://$NC_DOMAIN/standalone-signaling/\",\"verify\":true}],\"secret\":\"$SIGNALING_SECRET\"}" - php /var/www/html/occ config:app:set spreed stun_servers --value="$STUN_SERVERS" --output json - php /var/www/html/occ config:app:set spreed turn_servers --value="$TURN_SERVERS" --output json - php /var/www/html/occ config:app:set spreed signaling_servers --value="$SIGNALING_SERVERS" --output json + # Based on https://github.com/nextcloud/spreed/issues/960#issuecomment-416993435 + if [ -z "$(php /var/www/html/occ talk:turn:list --output="plain")" ]; then + # shellcheck disable=SC2153 + php /var/www/html/occ talk:turn:add turn "$TURN_DOMAIN:$TALK_PORT" "udp,tcp" --secret="$TURN_SECRET" + fi + STUN_SERVER="$(php /var/www/html/occ talk:stun:list --output="plain")" + if [ -z "$STUN_SERVER" ] || echo "$STUN_SERVER" | grep -oP '[a-zA-Z.:0-9]+' | grep -q "^stun.nextcloud.com:443$"; then + php /var/www/html/occ talk:stun:add "$TURN_DOMAIN:$TALK_PORT" + php /var/www/html/occ talk:stun:delete "stun.nextcloud.com:443" + fi + if ! php /var/www/html/occ talk:signaling:list --output="plain" | grep -q "https://$TALK_HOST$HPB_PATH"; then + php /var/www/html/occ talk:signaling:add "https://$TALK_HOST$HPB_PATH" "$SIGNALING_SECRET" --verify + fi else - if [ -d "/var/www/html/custom_apps/spreed" ]; then + if [ "$REMOVE_DISABLED_APPS" = yes ] && [ -d "/var/www/html/custom_apps/spreed" ]; then php /var/www/html/occ app:remove spreed fi fi +# Talk recording +if [ -d "/var/www/html/custom_apps/spreed" ]; then + if [ "$TALK_RECORDING_ENABLED" = 'yes' ]; then + while ! nc -z "$TALK_RECORDING_HOST" 1234; do + echo "Waiting for Talk Recording to become available..." + sleep 5 + done + # TODO: migrate to occ command if that becomes available + RECORDING_SERVERS_STRING="{\"servers\":[{\"server\":\"http://$TALK_RECORDING_HOST:1234/\",\"verify\":true}],\"secret\":\"$RECORDING_SECRET\"}" + php /var/www/html/occ config:app:set spreed recording_servers --value="$RECORDING_SERVERS_STRING" + else + if [ "$REMOVE_DISABLED_APPS" = yes ]; then + php /var/www/html/occ config:app:delete spreed recording_servers + fi + fi +fi + # Clamav if [ "$CLAMAV_ENABLED" = 'yes' ]; then - while ! nc -z "$CLAMAV_HOST" 3310; do - echo "waiting for clamav to become available..." + count=0 + while ! nc -z "$CLAMAV_HOST" 3310 && [ "$count" -lt 90 ]; do + echo "Waiting for ClamAV to become available..." + count=$((count+5)) sleep 5 done - if ! [ -d "/var/www/html/custom_apps/files_antivirus" ]; then - php /var/www/html/occ app:install files_antivirus - elif [ "$(php /var/www/html/occ config:app:get files_antivirus enabled)" = "no" ]; then - php /var/www/html/occ app:enable files_antivirus + if [ "$count" -ge 90 ]; then + bash /notify.sh "ClamAV did not start in time!" "Skipping initialization and disabling files_antivirus app." + php /var/www/html/occ app:disable files_antivirus else - php /var/www/html/occ app:update files_antivirus + if ! [ -d "/var/www/html/custom_apps/files_antivirus" ]; then + php /var/www/html/occ app:install files_antivirus + elif [ "$(php /var/www/html/occ config:app:get files_antivirus enabled)" != "yes" ]; then + php /var/www/html/occ app:enable files_antivirus + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update files_antivirus + fi + php /var/www/html/occ config:app:set files_antivirus av_mode --value="daemon" + php /var/www/html/occ config:app:set files_antivirus av_port --value="3310" + php /var/www/html/occ config:app:set files_antivirus av_host --value="$CLAMAV_HOST" + # av_stream_max_length must be synced with StreamMaxLength inside clamav + php /var/www/html/occ config:app:set files_antivirus av_stream_max_length --value="2147483648" + php /var/www/html/occ config:app:set files_antivirus av_max_file_size --value="-1" + php /var/www/html/occ config:app:set files_antivirus av_infected_action --value="only_log" + if [ -n "$CLAMAV_BLOCKLISTED_DIRECTORIES" ]; then + php /var/www/html/occ config:app:set files_antivirus av_blocklisted_directories --value="$CLAMAV_BLOCKLISTED_DIRECTORIES" + fi fi - php /var/www/html/occ config:app:set files_antivirus av_mode --value="daemon" - php /var/www/html/occ config:app:set files_antivirus av_port --value="3310" - php /var/www/html/occ config:app:set files_antivirus av_host --value="$CLAMAV_HOST" - php /var/www/html/occ config:app:set files_antivirus av_stream_max_length --value="104857600" - php /var/www/html/occ config:app:set files_antivirus av_max_file_size --value="-1" - php /var/www/html/occ config:app:set files_antivirus av_infected_action --value="only_log" else - if [ -d "/var/www/html/custom_apps/files_antivirus" ]; then + if [ "$REMOVE_DISABLED_APPS" = yes ] && [ -d "/var/www/html/custom_apps/files_antivirus" ]; then php /var/www/html/occ app:remove files_antivirus fi fi # Imaginary -if version_greater "$installed_version" "24.0.0.0"; then - if [ "$IMAGINARY_ENABLED" = 'yes' ]; then - php /var/www/html/occ config:system:set enabledPreviewProviders 0 --value="OC\\Preview\\Imaginary" - php /var/www/html/occ config:system:set preview_imaginary_url --value="http://$IMAGINARY_HOST:9000" - else +if [ "$IMAGINARY_ENABLED" = 'yes' ]; then + php /var/www/html/occ config:system:set enabledPreviewProviders 0 --value="OC\\Preview\\Imaginary" + php /var/www/html/occ config:system:set enabledPreviewProviders 23 --value="OC\\Preview\\ImaginaryPDF" + php /var/www/html/occ config:system:set preview_imaginary_url --value="http://$IMAGINARY_HOST:9000" + php /var/www/html/occ config:system:set preview_imaginary_key --value="$IMAGINARY_SECRET" +else + if [ -n "$(php /var/www/html/occ config:system:get preview_imaginary_url)" ]; then php /var/www/html/occ config:system:delete enabledPreviewProviders 0 php /var/www/html/occ config:system:delete preview_imaginary_url + php /var/www/html/occ config:system:delete enabledPreviewProviders 20 + php /var/www/html/occ config:system:delete enabledPreviewProviders 21 + php /var/www/html/occ config:system:delete enabledPreviewProviders 22 + php /var/www/html/occ config:system:delete enabledPreviewProviders 23 fi fi # Fulltextsearch if [ "$FULLTEXTSEARCH_ENABLED" = 'yes' ]; then - while ! nc -z "$FULLTEXTSEARCH_HOST" 9200; do - echo "waiting for Fulltextsearch to become available..." + count=0 + while ! nc -z "$FULLTEXTSEARCH_HOST" "$FULLTEXTSEARCH_PORT" && [ "$count" -lt 90 ]; do + echo "Waiting for Fulltextsearch to become available..." + count=$((count+5)) sleep 5 done - if ! [ -d "/var/www/html/custom_apps/fulltextsearch" ]; then - php /var/www/html/occ app:install fulltextsearch - elif [ "$(php /var/www/html/occ config:app:get fulltextsearch enabled)" = "no" ]; then - php /var/www/html/occ app:enable fulltextsearch + if [ "$count" -ge 90 ]; then + echo "Fulltextsearch did not start in time. Skipping initialization and disabling fulltextsearch apps." + php /var/www/html/occ app:disable fulltextsearch + php /var/www/html/occ app:disable fulltextsearch_elasticsearch + php /var/www/html/occ app:disable files_fulltextsearch else - php /var/www/html/occ app:update fulltextsearch - fi - if ! [ -d "/var/www/html/custom_apps/fulltextsearch_elasticsearch" ]; then - php /var/www/html/occ app:install fulltextsearch_elasticsearch - elif [ "$(php /var/www/html/occ config:app:get fulltextsearch_elasticsearch enabled)" = "no" ]; then - php /var/www/html/occ app:enable fulltextsearch_elasticsearch - else - php /var/www/html/occ app:update fulltextsearch_elasticsearch - fi - if ! [ -d "/var/www/html/custom_apps/files_fulltextsearch" ]; then - php /var/www/html/occ app:install files_fulltextsearch - elif [ "$(php /var/www/html/occ config:app:get files_fulltextsearch enabled)" = "no" ]; then - php /var/www/html/occ app:enable files_fulltextsearch - else - php /var/www/html/occ app:update files_fulltextsearch - fi - php /var/www/html/occ fulltextsearch:configure '{"search_platform":"OCA\\FullTextSearch_Elasticsearch\\Platform\\ElasticSearchPlatform"}' - php /var/www/html/occ fulltextsearch_elasticsearch:configure "{\"elastic_host\":\"http://$FULLTEXTSEARCH_HOST:9200\",\"elastic_index\":\"nextcloud-aio\"}" - php /var/www/html/occ files_fulltextsearch:configure "{\"files_pdf\":\"1\",\"files_office\":\"1\"}" + if [ -z "$FULLTEXTSEARCH_PROTOCOL" ]; then + FULLTEXTSEARCH_PROTOCOL="http" + fi + if ! [ -d "/var/www/html/custom_apps/fulltextsearch" ]; then + php /var/www/html/occ app:install fulltextsearch + elif [ "$(php /var/www/html/occ config:app:get fulltextsearch enabled)" != "yes" ]; then + php /var/www/html/occ app:enable fulltextsearch + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update fulltextsearch + fi + if ! [ -d "/var/www/html/custom_apps/fulltextsearch_elasticsearch" ]; then + php /var/www/html/occ app:install fulltextsearch_elasticsearch + elif [ "$(php /var/www/html/occ config:app:get fulltextsearch_elasticsearch enabled)" != "yes" ]; then + php /var/www/html/occ app:enable fulltextsearch_elasticsearch + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update fulltextsearch_elasticsearch + fi + if ! [ -d "/var/www/html/custom_apps/files_fulltextsearch" ]; then + php /var/www/html/occ app:install files_fulltextsearch + elif [ "$(php /var/www/html/occ config:app:get files_fulltextsearch enabled)" != "yes" ]; then + php /var/www/html/occ app:enable files_fulltextsearch + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update files_fulltextsearch + fi + php /var/www/html/occ fulltextsearch:configure '{"search_platform":"OCA\\FullTextSearch_Elasticsearch\\Platform\\ElasticSearchPlatform"}' + php /var/www/html/occ fulltextsearch_elasticsearch:configure "{\"elastic_host\":\"$FULLTEXTSEARCH_PROTOCOL://$FULLTEXTSEARCH_USER:$FULLTEXTSEARCH_PASSWORD@$FULLTEXTSEARCH_HOST:$FULLTEXTSEARCH_PORT\",\"elastic_index\":\"$FULLTEXTSEARCH_INDEX\"}" + php /var/www/html/occ files_fulltextsearch:configure "{\"files_pdf\":true,\"files_office\":true}" - # Do the index - if ! [ -f "/mnt/ncdata/fts-index.done" ]; then - echo "Waiting 10s before activating FTS..." - sleep 10 - echo "Activating fulltextsearch..." - if php /var/www/html/occ fulltextsearch:test && php /var/www/html/occ fulltextsearch:index; then - touch "/mnt/ncdata/fts-index.done" - else - echo "Fulltextsearch failed. Could not index." + # Do the index + if ! [ -f "$NEXTCLOUD_DATA_DIR/fts-index.done" ]; then + echo "Waiting 10 seconds before activating fulltextsearch..." + sleep 10 + echo "Activating fulltextsearch..." + if php /var/www/html/occ fulltextsearch:test && php /var/www/html/occ fulltextsearch:index "{\"errors\": \"reset\"}" --no-readline; then + touch "$NEXTCLOUD_DATA_DIR/fts-index.done" + else + echo "Fulltextsearch failed. Could not index." + echo "If you want to skip indexing in the future, see https://github.com/nextcloud/all-in-one/discussions/1709" + fi fi fi else - if [ -d "/var/www/html/custom_apps/fulltextsearch" ]; then - php /var/www/html/occ app:remove fulltextsearch + if [ "$REMOVE_DISABLED_APPS" = yes ]; then + if [ -d "/var/www/html/custom_apps/fulltextsearch" ]; then + php /var/www/html/occ app:remove fulltextsearch + fi + if [ -d "/var/www/html/custom_apps/fulltextsearch_elasticsearch" ]; then + php /var/www/html/occ app:remove fulltextsearch_elasticsearch + fi + if [ -d "/var/www/html/custom_apps/files_fulltextsearch" ]; then + php /var/www/html/occ app:remove files_fulltextsearch + fi fi - if [ -d "/var/www/html/custom_apps/fulltextsearch_elasticsearch" ]; then - php /var/www/html/occ app:remove fulltextsearch_elasticsearch +fi + +# Docker socket proxy +# app_api is a shipped app +if [ -d "/var/www/html/custom_apps/app_api" ]; then + php /var/www/html/occ app:disable app_api + rm -r "/var/www/html/custom_apps/app_api" +fi +if [ "$DOCKER_SOCKET_PROXY_ENABLED" = 'yes' ]; then + if [ "$(php /var/www/html/occ config:app:get app_api enabled)" != "yes" ]; then + php /var/www/html/occ app:enable app_api fi - if [ -d "/var/www/html/custom_apps/files_fulltextsearch" ]; then - php /var/www/html/occ app:remove files_fulltextsearch +else + if [ "$REMOVE_DISABLED_APPS" = yes ]; then + if [ "$(php /var/www/html/occ config:app:get app_api enabled)" != "no" ]; then + php /var/www/html/occ app:disable app_api + fi + fi +fi + +# Whiteboard app +if [ "$WHITEBOARD_ENABLED" = 'yes' ]; then + if ! [ -d "/var/www/html/custom_apps/whiteboard" ]; then + php /var/www/html/occ app:install whiteboard + elif [ "$(php /var/www/html/occ config:app:get whiteboard enabled)" != "yes" ]; then + php /var/www/html/occ app:enable whiteboard + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update whiteboard + fi + php /var/www/html/occ config:app:set whiteboard collabBackendUrl --value="https://$NC_DOMAIN/whiteboard" + php /var/www/html/occ config:app:set whiteboard jwt_secret_key --value="$WHITEBOARD_SECRET" +else + if [ "$REMOVE_DISABLED_APPS" = yes ] && [ -d "/var/www/html/custom_apps/whiteboard" ]; then + php /var/www/html/occ app:remove whiteboard fi fi # Remove the update skip file always -rm -f /mnt/ncdata/skip.update +rm -f "$NEXTCLOUD_DATA_DIR"/skip.update diff --git a/Containers/nextcloud/healthcheck.sh b/Containers/nextcloud/healthcheck.sh new file mode 100644 index 00000000..54c79dca --- /dev/null +++ b/Containers/nextcloud/healthcheck.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Set a default value for POSTGRES_PORT +if [ -z "$POSTGRES_PORT" ]; then + POSTGRES_PORT=5432 +fi + + +# POSTGRES_HOST must be set in the containers env vars and POSTGRES_PORT has a default above +# shellcheck disable=SC2153 +nc -z "$POSTGRES_HOST" "$POSTGRES_PORT" || exit 0 + +if ! nc -z 127.0.0.1 9000; then + exit 1 +fi diff --git a/Containers/nextcloud/notify-all.sh b/Containers/nextcloud/notify-all.sh new file mode 100644 index 00000000..f4dfa0fd --- /dev/null +++ b/Containers/nextcloud/notify-all.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +if [[ "$EUID" = 0 ]]; then + COMMAND=(sudo -E -u www-data php /var/www/html/occ) +else + COMMAND=(php /var/www/html/occ) +fi + +SUBJECT="$1" +MESSAGE="$2" + +if [ "$("${COMMAND[@]}" config:app:get notifications enabled)" = "no" ]; then + echo "Cannot send notification as notification app is not enabled." + exit 1 +fi + +echo "Posting notifications to all users..." +NC_USERS=$("${COMMAND[@]}" user:list | sed 's|^ - ||g' | sed 's|:.*||') +mapfile -t NC_USERS <<< "$NC_USERS" +for user in "${NC_USERS[@]}" +do + echo "Posting '$SUBJECT' to: $user" + "${COMMAND[@]}" notification:generate "$user" "$NC_DOMAIN: $SUBJECT" -l "$MESSAGE" --object-type='update' --object-id="$SUBJECT" +done + +echo "Done!" +exit 0 \ No newline at end of file diff --git a/Containers/nextcloud/notify.sh b/Containers/nextcloud/notify.sh index 3b0875b7..2ac4ceac 100644 --- a/Containers/nextcloud/notify.sh +++ b/Containers/nextcloud/notify.sh @@ -1,19 +1,25 @@ #!/bin/bash +if [[ "$EUID" = 0 ]]; then + COMMAND=(sudo -E -u www-data php /var/www/html/occ) +else + COMMAND=(php /var/www/html/occ) +fi + SUBJECT="$1" MESSAGE="$2" -if [ "$(php /var/www/html/occ config:app:get notifications enabled)" = "no" ]; then +if [ "$("${COMMAND[@]}" config:app:get notifications enabled)" = "no" ]; then echo "Cannot send notification as notification app is not enabled." exit 1 fi echo "Posting notifications to users that are admins..." -NC_USERS=$(php /var/www/html/occ user:list | sed 's|^ - ||g' | sed 's|:.*||') +NC_USERS=$("${COMMAND[@]}" user:list | sed 's|^ - ||g' | sed 's|:.*||') mapfile -t NC_USERS <<< "$NC_USERS" for user in "${NC_USERS[@]}" do - if php /var/www/html/occ user:info "$user" | cut -d "-" -f2 | grep -x -q " admin" + if "${COMMAND[@]}" user:info "$user" | cut -d "-" -f2 | grep -x -q " admin" then NC_ADMIN_USER+=("$user") fi @@ -22,7 +28,7 @@ done for admin in "${NC_ADMIN_USER[@]}" do echo "Posting '$SUBJECT' to: $admin" - php /var/www/html/occ notification:generate "$admin" "$NC_DOMAIN: $SUBJECT" -l "$MESSAGE" + "${COMMAND[@]}" notification:generate "$admin" "$NC_DOMAIN: $SUBJECT" -l "$MESSAGE" --object-type='update' --object-id="$SUBJECT" done echo "Done!" diff --git a/Containers/nextcloud/root.motd b/Containers/nextcloud/root.motd new file mode 100644 index 00000000..00cb4805 --- /dev/null +++ b/Containers/nextcloud/root.motd @@ -0,0 +1,4 @@ +Warning: You have logged in into the Nextcloud container as root user. +See https://github.com/nextcloud/all-in-one#how-to-run-occ-commands if you want to run occ commands. +Apart from that, you can use 'sudo -E -u www-data php occ ' in order to run occ commands. +Of course needs to be substituted with the command that you want to use. diff --git a/Containers/nextcloud/run-exec-commands.sh b/Containers/nextcloud/run-exec-commands.sh new file mode 100644 index 00000000..e8066881 --- /dev/null +++ b/Containers/nextcloud/run-exec-commands.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Wait until the apache container is ready +while ! nc -z "$APACHE_HOST" "$APACHE_PORT"; do + echo "Waiting for $APACHE_HOST to become available..." + sleep 15 +done + +if [ -n "$NEXTCLOUD_EXEC_COMMANDS" ]; then + echo "#!/bin/bash" > /tmp/nextcloud-exec-commands + echo "$NEXTCLOUD_EXEC_COMMANDS" >> /tmp/nextcloud-exec-commands + if ! grep "one-click-instance" /tmp/nextcloud-exec-commands; then + bash /tmp/nextcloud-exec-commands + rm /tmp/nextcloud-exec-commands + fi +else + # Collabora must work also if using manual-install + if [ "$COLLABORA_ENABLED" = yes ]; then + echo "Activating Collabora config..." + php /var/www/html/occ richdocuments:activate-config + fi +fi + +signal_handler() { + exit 0 +} + +trap signal_handler SIGINT SIGTERM + +sleep inf & +wait $! diff --git a/Containers/nextcloud/start.sh b/Containers/nextcloud/start.sh index 7cec862c..a5f38534 100644 --- a/Containers/nextcloud/start.sh +++ b/Containers/nextcloud/start.sh @@ -1,7 +1,14 @@ #!/bin/bash +# Set a default value for POSTGRES_PORT +if [ -z "$POSTGRES_PORT" ]; then + POSTGRES_PORT=5432 +fi + # Only start container if database is accessible -while ! nc -z "$POSTGRES_HOST" 5432; do +# POSTGRES_HOST must be set in the containers env vars and POSTGRES_PORT has a default above +# shellcheck disable=SC2153 +while ! sudo -E -u www-data nc -z "$POSTGRES_HOST" "$POSTGRES_PORT"; do echo "Waiting for database to start..." sleep 5 done @@ -10,30 +17,159 @@ done POSTGRES_USER="oc_$POSTGRES_USER" export POSTGRES_USER +# Check that db type is not empty +if [ -z "$DATABASE_TYPE" ]; then + export DATABASE_TYPE=postgres +fi + # Fix false database connection on old instances if [ -f "/var/www/html/config/config.php" ]; then sleep 2 - while ! psql -d "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:5432/$POSTGRES_DB" -c "select now()"; do + while ! sudo -E -u www-data psql -d "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB" -c "select now()"; do echo "Waiting for the database to start..." sleep 5 done - sed -i "s|'dbuser'.*=>.*$|'dbuser' => '$POSTGRES_USER',|" /var/www/html/config/config.php - sed -i "s|'dbpassword'.*=>.*$|'dbpassword' => '$POSTGRES_PASSWORD',|" /var/www/html/config/config.php + if [ "$POSTGRES_USER" = "oc_nextcloud" ] && [ "$POSTGRES_DB" = "nextcloud_database" ] && echo "$POSTGRES_PASSWORD" | grep -q '^[a-z0-9]\+$'; then + # This was introduced with https://github.com/nextcloud/all-in-one/pull/218 + sed -i "s|'dbuser'.*=>.*$|'dbuser' => '$POSTGRES_USER',|" /var/www/html/config/config.php + sed -i "s|'dbpassword'.*=>.*$|'dbpassword' => '$POSTGRES_PASSWORD',|" /var/www/html/config/config.php + sed -i "s|'db_name'.*=>.*$|'db_name' => '$POSTGRES_DB',|" /var/www/html/config/config.php + fi +fi + +# Trust additional Cacerts, if the user provided $TRUSTED_CACERTS_DIR +if [ -n "$TRUSTED_CACERTS_DIR" ]; then + echo "User required to trust additional CA certificates, running 'update-ca-certificates.'" + update-ca-certificates +fi + +# Check if /dev/dri device is present and apply correct permissions +set -x +if ! [ -f "/dev-dri-group-was-added" ] && [ -n "$(find /dev -maxdepth 1 -mindepth 1 -name dri)" ] && [ -n "$(find /dev/dri -maxdepth 1 -mindepth 1 -name renderD128)" ]; then + # From https://memories.gallery/hw-transcoding/#docker-installations + GID="$(stat -c "%g" /dev/dri/renderD128)" + groupadd -g "$GID" render2 || true # sometimes this is needed + GROUP="$(getent group "$GID" | cut -d: -f1)" + usermod -aG "$GROUP" www-data + touch "/dev-dri-group-was-added" +fi +set +x + +# Check datadir permissions +sudo -E -u www-data touch "$NEXTCLOUD_DATA_DIR/this-is-a-test-file" &>/dev/null +if ! [ -f "$NEXTCLOUD_DATA_DIR/this-is-a-test-file" ]; then + chown -R www-data:root "$NEXTCLOUD_DATA_DIR" + chmod 750 -R "$NEXTCLOUD_DATA_DIR" +fi +sudo -E -u www-data rm -f "$NEXTCLOUD_DATA_DIR/this-is-a-test-file" + +# Install additional dependencies +if [ -n "$ADDITIONAL_APKS" ]; then + if ! [ -f "/additional-apks-are-installed" ]; then + # Allow to disable imagemagick without having to download it each time + if ! echo "$ADDITIONAL_APKS" | grep -q imagemagick; then + apk del imagemagick imagemagick-svg imagemagick-heic imagemagick-tiff; + fi + read -ra ADDITIONAL_APKS_ARRAY <<< "$ADDITIONAL_APKS" + for app in "${ADDITIONAL_APKS_ARRAY[@]}"; do + if [ "$app" != imagemagick ]; then + echo "Installing $app via apk..." + if ! apk add --no-cache "$app" >/dev/null; then + echo "The packet $app was not installed!" + fi + fi + done + fi + touch /additional-apks-are-installed +fi + +# Install additional php extensions +if [ -n "$ADDITIONAL_PHP_EXTENSIONS" ]; then + if ! [ -f "/additional-php-extensions-are-installed" ]; then + # Allow to disable imagick without having to enable it each time + if ! echo "$ADDITIONAL_PHP_EXTENSIONS" | grep -q imagick; then + # Remove the ini file as there is no docker-php-ext-disable script available + rm /usr/local/etc/php/conf.d/docker-php-ext-imagick.ini + fi + read -ra ADDITIONAL_PHP_EXTENSIONS_ARRAY <<< "$ADDITIONAL_PHP_EXTENSIONS" + for app in "${ADDITIONAL_PHP_EXTENSIONS_ARRAY[@]}"; do + if [ "$app" = imagick ]; then + # imagick is already enabled by default, so does not need to be enabled anymore. + continue + fi + # shellcheck disable=SC2086 + if [ "$PHP_DEPS_ARE_INSTALLED" != 1 ]; then + echo "Installing PHP build dependencies..." + if ! apk add --no-cache --virtual .build-deps \ + libxml2-dev \ + autoconf \ + $PHPIZE_DEPS >/dev/null; then + echo "Could not install build-deps!" + fi + PHP_DEPS_ARE_INSTALLED=1 + fi + if [ "$app" = inotify ]; then + echo "Installing $app via PECL..." + pecl install "$app" >/dev/null + if ! docker-php-ext-enable "$app" >/dev/null; then + echo "Could not install PHP extension $app!" + fi + elif [ "$app" = soap ]; then + echo "Installing $app from core..." + if ! docker-php-ext-install -j "$(nproc)" "$app" >/dev/null; then + echo "Could not install PHP extension $app!" + fi + else + echo "Installing PHP extension $app ..." + if ! docker-php-ext-install -j "$(nproc)" "$app" >/dev/null; then + echo "Could not install $app from core. Trying to install from PECL..." + pecl install "$app" >/dev/null + if ! docker-php-ext-enable "$app" >/dev/null; then + echo "Could also not install $app from PECL. The PHP extensions was not installed!" + fi + fi + fi + done + if [ "$PHP_DEPS_ARE_INSTALLED" = 1 ]; then + rm -rf /tmp/pear + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )"; + # shellcheck disable=SC2086 + apk add --no-cache --virtual .nextcloud-phpext-rundeps $runDeps >/dev/null + apk del .build-deps >/dev/null + fi + fi + touch /additional-php-extensions-are-installed fi # Run original entrypoint -if ! bash /entrypoint.sh; then +if ! sudo -E -u www-data bash /entrypoint.sh; then exit 1 fi -# Correctly set CPU_ARCH for notify_push -CPU_ARCH="$(uname -m)" -export CPU_ARCH -if [ -z "$CPU_ARCH" ]; then - echo "Could not get processor architecture. Exiting." - exit 1 -elif [ "$CPU_ARCH" != "x86_64" ]; then - export CPU_ARCH="aarch64" -fi +while [ "$THIS_IS_AIO" = "true" ] && [ -z "$(dig nextcloud-aio-apache A +short +search)" ]; do + echo "Waiting for nextcloud-aio-apache to start..." + sleep 5 +done -exec "$@" \ No newline at end of file +set -x +# shellcheck disable=SC2235 +if [ "$THIS_IS_AIO" = "true" ] && [ "$APACHE_PORT" = 443 ]; then + IPv4_ADDRESS_APACHE="$(dig nextcloud-aio-apache A +short +search | grep '^[0-9.]\+$' | sort | head -n1)" + IPv6_ADDRESS_APACHE="$(dig nextcloud-aio-apache AAAA +short +search | grep '^[0-9a-f:]\+$' | sort | head -n1)" + IPv4_ADDRESS_MASTERCONTAINER="$(dig nextcloud-aio-mastercontainer A +short +search | grep '^[0-9.]\+$' | sort | head -n1)" + IPv6_ADDRESS_MASTERCONTAINER="$(dig nextcloud-aio-mastercontainer AAAA +short +search | grep '^[0-9a-f:]\+$' | sort | head -n1)" + + sed -i "s|^;listen.allowed_clients|listen.allowed_clients|" /usr/local/etc/php-fpm.d/www.conf + sed -i "s|listen.allowed_clients.*|listen.allowed_clients = 127.0.0.1,::1,$IPv4_ADDRESS_APACHE,$IPv6_ADDRESS_APACHE,$IPv4_ADDRESS_MASTERCONTAINER,$IPv6_ADDRESS_MASTERCONTAINER|" /usr/local/etc/php-fpm.d/www.conf + sed -i "/^listen.allowed_clients/s/,,/,/g" /usr/local/etc/php-fpm.d/www.conf + sed -i "/^listen.allowed_clients/s/,$//" /usr/local/etc/php-fpm.d/www.conf + grep listen.allowed_clients /usr/local/etc/php-fpm.d/www.conf +fi +set +x + +exec "$@" diff --git a/Containers/nextcloud/supervisord.conf b/Containers/nextcloud/supervisord.conf index f411e59b..1db885e9 100644 --- a/Containers/nextcloud/supervisord.conf +++ b/Containers/nextcloud/supervisord.conf @@ -7,6 +7,7 @@ childlogdir=/var/log/supervisord/ logfile_maxbytes=50MB ; maximum size of logfile before rotation logfile_backups=10 ; number of backed up logfiles loglevel=error +user=root [program:php-fpm] stdout_logfile=/dev/stdout @@ -14,6 +15,7 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 command=php-fpm +user=root [program:cron] stdout_logfile=/dev/stdout @@ -21,17 +23,23 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 command=/cron.sh +user=www-data -[program:notify-push] +[program:run-exec-commands] stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -command=/var/www/html/custom_apps/notify_push/bin/%(ENV_CPU_ARCH)s/notify_push /var/www/html/config/config.php --port 7867 --redis-url redis://:%(ENV_REDIS_HOST_PASSWORD)s@%(ENV_REDIS_HOST)s +command=/run-exec-commands.sh +user=www-data -[program:activate-collabora] +# This is a hack but no better solution is there +[program:is-nextcloud-online] stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -command=/activate-collabora.sh +# Restart the netcat command once a day to ensure that it stays reachable +# See https://github.com/nextcloud/all-in-one/issues/6334 +command=timeout 86400 nc -lk 9001 +user=www-data diff --git a/Containers/notify-push/Dockerfile b/Containers/notify-push/Dockerfile new file mode 100644 index 00000000..838c847c --- /dev/null +++ b/Containers/notify-push/Dockerfile @@ -0,0 +1,27 @@ +# syntax=docker/dockerfile:latest +FROM alpine:3.23.3 + +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + ca-certificates \ + netcat-openbsd \ + tzdata \ + bash \ + jq \ + openssl; \ +# Give root a random password + echo "root:$(openssl rand -base64 12)" | chpasswd; \ + apk del --no-cache \ + openssl; + +USER 33 +ENTRYPOINT ["/start.sh"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/notify-push/healthcheck.sh b/Containers/notify-push/healthcheck.sh new file mode 100644 index 00000000..e2539436 --- /dev/null +++ b/Containers/notify-push/healthcheck.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if ! nc -z "$NEXTCLOUD_HOST" 9001; then + exit 0 +fi + +nc -z 127.0.0.1 7867 || exit 1 diff --git a/Containers/notify-push/start.sh b/Containers/notify-push/start.sh new file mode 100644 index 00000000..26d74333 --- /dev/null +++ b/Containers/notify-push/start.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +if [ -z "$NEXTCLOUD_HOST" ]; then + echo "NEXTCLOUD_HOST needs to be provided. Exiting!" + exit 1 +elif [ -z "$POSTGRES_HOST" ]; then + echo "POSTGRES_HOST needs to be provided. Exiting!" + exit 1 +elif [ -z "$REDIS_HOST" ]; then + echo "REDIS_HOST needs to be provided. Exiting!" + exit 1 +fi + +# Only start container if nextcloud is accessible +while ! nc -z "$NEXTCLOUD_HOST" 9001; do + echo "Waiting for Nextcloud to start..." + sleep 5 +done + +# Correctly set CPU_ARCH for notify_push +CPU_ARCH="$(uname -m)" +export CPU_ARCH +if [ -z "$CPU_ARCH" ]; then + echo "Could not get processor architecture. Exiting." + exit 1 +elif [ "$CPU_ARCH" != "x86_64" ]; then + export CPU_ARCH="aarch64" +fi + +# Add warning +if ! [ -f /nextcloud/custom_apps/notify_push/bin/"$CPU_ARCH"/notify_push ]; then + echo "The notify_push binary was not found." + echo "Most likely is DNS resolution not working correctly." + echo "You can try to fix this by configuring a DNS server globally in dockers daemon.json." + echo "See https://dockerlabs.collabnix.com/intermediate/networking/Configuring_DNS.html" + echo "Afterwards a restart of docker should automatically resolve this." + echo "Additionally, make sure to disable VPN software that might be running on your server" + echo "Also check your firewall if it blocks connections to github" + echo "If it should still not work afterwards, feel free to create a new thread at https://github.com/nextcloud/all-in-one/discussions/new?category=questions and post the Nextcloud container logs there." + echo "" + echo "" + exit 1 +fi + +echo "notify-push was started" + +# Set a default value for POSTGRES_PORT +if [ -z "$POSTGRES_PORT" ]; then + POSTGRES_PORT=5432 +fi +# Set a default for redis db index +if [ -z "$REDIS_DB_INDEX" ]; then + REDIS_DB_INDEX=0 +fi +# Set a default value for REDIS_PORT +if [ -z "$REDIS_PORT" ]; then + REDIS_PORT=6379 +fi +# Set a default for db type +if [ -z "$DATABASE_TYPE" ]; then + DATABASE_TYPE=postgres +elif [ "$DATABASE_TYPE" != postgres ] && [ "$DATABASE_TYPE" != mysql ]; then + echo "DB type must be either postgres or mysql" + exit 1 +fi + +# Use the correct Postgres username +if [ "$POSTGRES_USER" = nextcloud ]; then + POSTGRES_USER="oc_$POSTGRES_USER" + export POSTGRES_USER +fi + +# URL-encode passwords +POSTGRES_PASSWORD="$(jq -rn --arg v "$POSTGRES_PASSWORD" '$v|@uri')" +REDIS_HOST_PASSWORD="$(jq -rn --arg v "$REDIS_HOST_PASSWORD" '$v|@uri')" + +# Postgres root cert +if [ -f "/nextcloud/data/certificates/POSTGRES" ]; then + CERT_OPTIONS="?sslmode=verify-ca&sslrootcert=/nextcloud/data/certificates/ca-bundle.crt" +# Mysql root cert +elif [ -f "/nextcloud/data/certificates/MYSQL" ]; then + CERT_OPTIONS="?sslmode=verify-ca&ssl-ca=/nextcloud/data/certificates/ca-bundle.crt" +fi + +# Set sensitive values as env +export DATABASE_URL="$DATABASE_TYPE://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB$CERT_OPTIONS" +export REDIS_URL="redis://$REDIS_USER:$REDIS_HOST_PASSWORD@$REDIS_HOST:$REDIS_PORT/$REDIS_DB_INDEX" + +# Run it +/nextcloud/custom_apps/notify_push/bin/"$CPU_ARCH"/notify_push \ + --database-prefix="oc_" \ + --nextcloud-url "https://$NC_DOMAIN" \ + --port 7867 + +exec "$@" diff --git a/Containers/onlyoffice/Dockerfile b/Containers/onlyoffice/Dockerfile index 6be6b60e..13b4d456 100644 --- a/Containers/onlyoffice/Dockerfile +++ b/Containers/onlyoffice/Dockerfile @@ -1,4 +1,12 @@ +# syntax=docker/dockerfile:latest # From https://github.com/ONLYOFFICE/Docker-DocumentServer/blob/master/Dockerfile -FROM onlyoffice/documentserver:7.1.1.23 +FROM onlyoffice/documentserver:9.2.1.1 -HEALTHCHECK CMD curl -skfI localhost || exit 1 \ No newline at end of file +# USER root is probably used + +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +HEALTHCHECK --start-period=60s --retries=9 CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/onlyoffice/healthcheck.sh b/Containers/onlyoffice/healthcheck.sh new file mode 100644 index 00000000..7a9d79d0 --- /dev/null +++ b/Containers/onlyoffice/healthcheck.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +nc -z 127.0.0.1 80 || exit 1 diff --git a/Containers/postgresql/Dockerfile b/Containers/postgresql/Dockerfile index ea83de61..56090f26 100644 --- a/Containers/postgresql/Dockerfile +++ b/Containers/postgresql/Dockerfile @@ -1,35 +1,48 @@ -# From https://github.com/docker-library/postgres/blob/master/13/alpine/Dockerfile -FROM postgres:14.5-alpine +# syntax=docker/dockerfile:latest +# From https://github.com/docker-library/postgres/blob/master/17/alpine3.23/Dockerfile +FROM postgres:17.7-alpine -RUN apk add --update --no-cache bash openssl shadow netcat-openbsd grep mawk +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh +COPY --chmod=775 init-user-db.sh /docker-entrypoint-initdb.d/init-user-db.sh -# We need to use the same gid and uid as on old installations RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + bash \ + openssl \ + shadow \ + grep; \ + \ +# We need to use the same gid and uid as on old installations deluser postgres; \ groupmod -g 9999 ping; \ addgroup -g 999 -S postgres; \ - adduser -u 999 -S -D -G postgres -H -h /var/lib/postgresql -s /bin/sh postgres - + adduser -u 999 -S -D -G postgres -H -h /var/lib/postgresql -s /bin/sh postgres; \ + apk del --no-cache shadow; \ + \ # Fix default permissions -RUN set -ex; \ chown -R postgres:postgres /var/lib/postgresql; \ chown -R postgres:postgres /var/run/postgresql; \ - chown -R postgres:postgres "$PGDATA" - -COPY start.sh /usr/bin/ -COPY init-user-db.sh /docker-entrypoint-initdb.d/ -RUN chmod +x /usr/bin/start.sh; \ - chmod +xr /docker-entrypoint-initdb.d/init-user-db.sh - -RUN mkdir /mnt/data; \ - chown postgres:postgres /mnt/data; + chmod -R 777 /var/run/postgresql; \ + chown -R postgres:postgres "$PGDATA"; \ + \ + mkdir /mnt/data; \ + chown postgres:postgres /mnt/data; \ + \ +# Give root a random password + echo "root:$(openssl rand -base64 12)" | chpasswd; \ + apk --no-cache del openssl; \ + \ +# Get rid of unused binaries + rm -f /usr/local/bin/gosu /usr/local/bin/su-exec; VOLUME /mnt/data -# Give root a random password -RUN echo "root:$(openssl rand -base64 12)" | chpasswd +USER 999 +ENTRYPOINT ["/start.sh"] -USER postgres -ENTRYPOINT ["start.sh"] - -HEALTHCHECK CMD psql -d "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "select now()" || exit 1 \ No newline at end of file +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/postgresql/healthcheck.sh b/Containers/postgresql/healthcheck.sh new file mode 100644 index 00000000..9f303a3a --- /dev/null +++ b/Containers/postgresql/healthcheck.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +test -f "/mnt/data/backup-is-running" && exit 0 + +psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:11000/$POSTGRES_DB" -c "select now()" && exit 0 + +psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:5432/$POSTGRES_DB" -c "select now()" || exit 1 diff --git a/Containers/postgresql/init-user-db.sh b/Containers/postgresql/init-user-db.sh index 1f09770b..cfd3827a 100644 --- a/Containers/postgresql/init-user-db.sh +++ b/Containers/postgresql/init-user-db.sh @@ -1,9 +1,15 @@ #!/bin/bash set -ex +touch "$DUMP_DIR/initialization.failed" + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE USER "oc_$POSTGRES_USER" WITH PASSWORD '$POSTGRES_PASSWORD' CREATEDB; ALTER DATABASE "$POSTGRES_DB" OWNER TO "oc_$POSTGRES_USER"; + GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO "oc_$POSTGRES_USER"; + GRANT ALL PRIVILEGES ON SCHEMA public TO "oc_$POSTGRES_USER"; EOSQL +rm "$DUMP_DIR/initialization.failed" + set +ex diff --git a/Containers/postgresql/start.sh b/Containers/postgresql/start.sh index b2e25a1b..551bb10e 100644 --- a/Containers/postgresql/start.sh +++ b/Containers/postgresql/start.sh @@ -2,13 +2,15 @@ # Variables DATADIR="/var/lib/postgresql/data" -DUMP_DIR="/mnt/data" +export DUMP_DIR="/mnt/data" DUMP_FILE="$DUMP_DIR/database-dump.sql" export PGPASSWORD="$POSTGRES_PASSWORD" # Don't start database as long as backup is running while [ -f "$DUMP_DIR/backup-is-running" ]; do echo "Waiting for backup container to finish..." + echo "If this is incorrect because the backup container is not running anymore (because it was forcefully killed), you might delete the lock file:" + echo "sudo docker exec --user root nextcloud-aio-database rm /mnt/data/backup-is-running" sleep 10 done @@ -18,6 +20,23 @@ if ! [ -w "$DUMP_DIR" ]; then exit 1 fi +# Don't start if import failed +if [ -f "$DUMP_DIR/import.failed" ]; then + echo "The database import failed. Please restore a backup and try again." + echo "For further clues on what went wrong, look at the logs above." + exit 1 +fi + +# Don't start if initialization failed +if [ -f "$DUMP_DIR/initialization.failed" ]; then + echo "The database initialization failed. Most likely was a wrong timezone selected." + echo "The selected timezone is '$TZ'." + echo "Please check if it is in the 'TZ identifier' column of the timezone list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List" + echo "For further clues on what went wrong, look at the logs above." + echo "You might start again from scratch by following https://github.com/nextcloud/all-in-one#how-to-properly-reset-the-instance and selecting a proper timezone." + exit 1 +fi + # Delete the datadir once (needed for setting the correct credentials on old instances once) if ! [ -f "$DUMP_DIR/export.failed" ] && ! [ -f "$DUMP_DIR/initial-cleanup-done" ]; then set -ex @@ -43,9 +62,16 @@ if ( [ -f "$DATADIR/PG_VERSION" ] && [ "$PG_MAJOR" != "$(cat "$DATADIR/PG_VERSIO exit 1 fi + # Write output to logfile. + exec > >(tee -i "$DUMP_DIR/database-import.log") + exec 2>&1 + # Inform echo "Restoring from database dump." + # Add import.failed file + touch "$DUMP_DIR/import.failed" + # Exit if any command fails set -ex @@ -59,26 +85,33 @@ if ( [ -f "$DATADIR/PG_VERSION" ] && [ "$PG_MAJOR" != "$(cat "$DATADIR/PG_VERSIO exec docker-entrypoint.sh postgres & # Wait for creation - while ! nc -z localhost 11000; do + while ! psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:11000/$POSTGRES_DB" -c "select now()"; do echo "Waiting for the database to start." sleep 5 done # Check if the line we grep for later on is there GREP_STRING='Name: oc_appconfig; Type: TABLE; Schema: public; Owner:' - if ! grep -q "$GREP_STRING" "$DUMP_FILE"; then + if ! grep -qa "$GREP_STRING" "$DUMP_FILE"; then echo "The needed oc_appconfig line is not there which is unexpected." echo "Please report this to https://github.com/nextcloud/all-in-one/issues. Thanks!" exit 1 fi # Get the Owner - DB_OWNER="$(grep "$GREP_STRING" "$DUMP_FILE" | grep -oP 'Owner:.*$' | sed 's|Owner:||;s| ||g')" - if [ "$DB_OWNER" != "oc_$POSTGRES_USER" ]; then + DB_OWNER="$(grep -a "$GREP_STRING" "$DUMP_FILE" | head -1 | grep -oP 'Owner:.*$' | sed 's|Owner:||;s|[[:space:]]||g')" + if [ "$DB_OWNER" = "$POSTGRES_USER" ]; then + echo "Unfortunately was the found database owner of the dump file the same as the POSTGRES_USER $POSTGRES_USER" + echo "It is not possible to import a database dump from this database owner." + echo "However you might rename the owner in the dumpfile to something else." + exit 1 + elif [ "$DB_OWNER" != "oc_$POSTGRES_USER" ]; then DIFFERENT_DB_OWNER=1 psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE USER "$DB_OWNER" WITH PASSWORD '$POSTGRES_PASSWORD' CREATEDB; ALTER DATABASE "$POSTGRES_DB" OWNER TO "$DB_OWNER"; + GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO "$DB_OWNER"; + GRANT ALL PRIVILEGES ON SCHEMA public TO "$DB_OWNER"; EOSQL fi @@ -95,13 +128,18 @@ EOSQL fi # Shut down the database to be able to start it again - pg_ctl stop -m fast + # The smart mode disallows new connections, then waits for all existing clients to disconnect and any online backup to finish + # Wait for 1800s to make sure that a checkpoint is completed successfully + pg_ctl stop -m smart -t 1800 # Change database port back to default export PGPORT=5432 # Don't exit if command fails anymore set +ex + + # Remove import failed file if everything went correctly + rm "$DUMP_DIR/import.failed" fi # Cover the last case @@ -110,32 +148,51 @@ if ! [ -f "$DATADIR/PG_VERSION" ] && ! [ -f "$DUMP_FILE" ]; then rm -rf "${DATADIR:?}/"* fi -echo "Setting max connections..." -MEMORY=$(mawk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo) -MAX_CONNECTIONS=$((MEMORY/50+3)) -if [ -n "$MAX_CONNECTIONS" ]; then - sed -i "s|^max_connections =.*|max_connections = $MAX_CONNECTIONS|" "/var/lib/postgresql/data/postgresql.conf" +# Modify postgresql.conf +if [ -f "/var/lib/postgresql/data/postgresql.conf" ]; then + echo "Setting postgres values..." + + # Sync this with max pm.max_children and MaxRequestWorkers + # 5000 connections is apparently the highest possible value with postgres so set it to that so that we don't run into a limit here. + # We don't actually expect so many connections but don't want to limit it artificially because people will report issues otherwise + # Also connections should usually be closed again after the process is done + # If we should actually exceed this limit, it is definitely a bug in Nextcloud server or some of its apps that does not close connections correctly and not a bug in AIO + sed -i "s|^max_connections =.*|max_connections = 5000|" "/var/lib/postgresql/data/postgresql.conf" + + # Do not log checkpoints + if grep -q "#log_checkpoints" /var/lib/postgresql/data/postgresql.conf; then + sed -i 's|#log_checkpoints.*|log_checkpoints = off|' /var/lib/postgresql/data/postgresql.conf + fi + + # Closing idling connections automatically seems to break any logic so was reverted again to default where it is disabled + if grep -q "^idle_session_timeout" /var/lib/postgresql/data/postgresql.conf; then + sed -i 's|^idle_session_timeout.*|#idle_session_timeout|' /var/lib/postgresql/data/postgresql.conf + fi fi +do_database_dump() { + set -x + rm -f "$DUMP_FILE.temp" + touch "$DUMP_DIR/export.failed" + if pg_dump --username "$POSTGRES_USER" "$POSTGRES_DB" > "$DUMP_FILE.temp"; then + rm -f "$DUMP_FILE" + mv "$DUMP_FILE.temp" "$DUMP_FILE" + pg_ctl stop -m fast + rm "$DUMP_DIR/export.failed" + echo 'Database dump successful!' + set +x + exit 0 + else + pg_ctl stop -m fast + echo "Database dump unsuccessful!" + set +x + exit 1 + fi +} + # Catch docker stop attempts -trap 'true' SIGINT SIGTERM +trap do_database_dump SIGINT SIGTERM # Start the database exec docker-entrypoint.sh postgres & wait $! - -# Continue with shutdown procedure: do database dump, etc. -rm -f "$DUMP_FILE.temp" -touch "$DUMP_DIR/export.failed" -if pg_dump --username "$POSTGRES_USER" "$POSTGRES_DB" > "$DUMP_FILE.temp"; then - rm -f "$DUMP_FILE" - mv "$DUMP_FILE.temp" "$DUMP_FILE" - pg_ctl stop -m fast - rm "$DUMP_DIR/export.failed" - echo 'Database dump successful!' - exit 0 -else - pg_ctl stop -m fast - echo "Database dump unsuccessful!" - exit 1 -fi diff --git a/Containers/redis/Dockerfile b/Containers/redis/Dockerfile index 620f3739..cc9181ad 100644 --- a/Containers/redis/Dockerfile +++ b/Containers/redis/Dockerfile @@ -1,15 +1,25 @@ -# From https://github.com/docker-library/redis/blob/master/6.2/alpine/Dockerfile -FROM redis:6.2.7-alpine +# syntax=docker/dockerfile:latest +# From https://github.com/redis/docker-library-redis/blob/release/8.2/alpine/Dockerfile +FROM redis:8.2.3-alpine -RUN apk add --update --no-cache openssl bash - -COPY start.sh /usr/bin/ -RUN chmod +x /usr/bin/start.sh +COPY --chmod=775 start.sh /start.sh +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache openssl bash; \ + \ # Give root a random password -RUN echo "root:$(openssl rand -base64 12)" | chpasswd + echo "root:$(openssl rand -base64 12)" | chpasswd; \ + \ +# Get rid of unused binaries + rm -f /usr/local/bin/gosu; -USER redis -ENTRYPOINT ["start.sh"] +COPY --chmod=775 healthcheck.sh /healthcheck.sh -HEALTHCHECK CMD redis-cli -a $REDIS_HOST_PASSWORD PING || exit 1 \ No newline at end of file +USER 999 +ENTRYPOINT ["/start.sh"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/redis/healthcheck.sh b/Containers/redis/healthcheck.sh new file mode 100644 index 00000000..6588229f --- /dev/null +++ b/Containers/redis/healthcheck.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +redis-cli -a "$REDIS_HOST_PASSWORD" PING || exit 1 diff --git a/Containers/redis/start.sh b/Containers/redis/start.sh index 601d736a..69764c1a 100644 --- a/Containers/redis/start.sh +++ b/Containers/redis/start.sh @@ -1,10 +1,17 @@ #!/bin/bash +# Show wiki if vm.overcommit is disabled +if [ "$(sysctl -n vm.overcommit_memory)" != "1" ]; then + echo "Memory overcommit is disabled but necessary for safe operation" + echo "See https://github.com/nextcloud/all-in-one/discussions/1731 how to enable overcommit" +fi + # Run redis with a password if provided +echo "Redis has started" if [ -n "$REDIS_HOST_PASSWORD" ]; then - exec redis-server --requirepass "$REDIS_HOST_PASSWORD" + exec redis-server --requirepass "$REDIS_HOST_PASSWORD" --loglevel warning else - exec redis-server + exec redis-server --loglevel warning fi exec "$@" diff --git a/Containers/talk-recording/Dockerfile b/Containers/talk-recording/Dockerfile new file mode 100644 index 00000000..8df5b89e --- /dev/null +++ b/Containers/talk-recording/Dockerfile @@ -0,0 +1,62 @@ +# syntax=docker/dockerfile:latest +FROM python:3.14.2-alpine3.23 + +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +ENV RECORDING_VERSION=v0.2.1 +ENV ALLOW_ALL=false +ENV HPB_PROTOCOL=https +ENV NC_PROTOCOL=https +ENV SKIP_VERIFY=false +ENV HPB_PATH=/standalone-signaling/ + +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + ca-certificates \ + tzdata \ + bash \ + xvfb \ + ffmpeg \ + firefox \ + bind-tools \ + netcat-openbsd \ + git \ + wget \ + shadow \ + pulseaudio \ + openssl \ + build-base \ + linux-headers \ + geckodriver; \ + useradd -d /tmp --system recording -u 122; \ +# Give root a random password + echo "root:$(openssl rand -base64 12)" | chpasswd; \ + git clone --recursive https://github.com/nextcloud/nextcloud-talk-recording --depth=1 --single-branch --branch "$RECORDING_VERSION" /src; \ + python3 -m pip install --no-cache-dir /src; \ + rm -rf /src; \ + touch /etc/recording.conf; \ + chown recording:recording -R \ + /tmp /etc/recording.conf; \ + mkdir -p /conf; \ + chmod 777 /conf; \ + chmod 777 /tmp; \ + apk del --no-cache \ + git \ + wget \ + shadow \ + openssl \ + build-base \ + linux-headers; + +VOLUME /tmp +WORKDIR /tmp +USER 122 +ENTRYPOINT ["/start.sh"] +CMD ["python", "-m", "nextcloud.talk.recording", "--config", "/conf/recording.conf"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/talk-recording/healthcheck.sh b/Containers/talk-recording/healthcheck.sh new file mode 100644 index 00000000..8397ab3c --- /dev/null +++ b/Containers/talk-recording/healthcheck.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +nc -z 127.0.0.1 1234 || exit 1 diff --git a/Containers/talk-recording/recording.conf b/Containers/talk-recording/recording.conf new file mode 100644 index 00000000..cc8bd495 --- /dev/null +++ b/Containers/talk-recording/recording.conf @@ -0,0 +1,169 @@ +# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +[logs] +# Log level based on numeric values of Python logging levels: +# - Critical: 50 +# - Error: 40 +# - Warning: 30 +# - Info: 20 +# - Debug: 10 +# - Not set: 0 +#level = 20 + +[http] +# IP and port to listen on for HTTP requests. +#listen = 127.0.0.1:8000 + +[app] +# Comma separated list of trusted proxies (IPs or CIDR networks) that may set +# the "X-Forwarded-For" header. +#trustedproxies = + +[backend] +# Allow any hostname as backend endpoint. This is extremely insecure and should +# only be used during development. +#allowall = false + +# Common shared secret for requests from and to the backend servers if +# "allowall" is enabled. This must be the same value as configured in the +# Nextcloud admin ui. +#secret = the-shared-secret + +# Comma-separated list of backend ids allowed to connect. +#backends = backend-id, another-backend + +# If set to "true", certificate validation of backend endpoints will be skipped. +# This should only be enabled during development, e.g. to work with self-signed +# certificates. +# Overridable by backend. +#skipverify = false + +# Maximum allowed size in bytes for messages sent by the backend. +# Overridable by backend. +#maxmessagesize = 1024 + +# Width for recorded videos. +# Overridable by backend. +#videowidth = 1920 + +# Height for recorded videos. +# Overridable by backend. +#videoheight = 1080 + +# Temporary directory used to store recordings until uploaded. It must be +# writable by the user running the recording server. +# Overridable by backend. +#directory = /tmp + +# Backend configurations as defined in the "[backend]" section above. The +# section names must match the ids used in "backends" above. +#[backend-id] +# URL of the Nextcloud instance +#url = https://cloud.domain.invalid + +# Shared secret for requests from and to the backend servers. This must be the +# same value as configured in the Nextcloud admin ui. +#secret = the-shared-secret + +#[another-backend] +# URL of the Nextcloud instance +#url = https://cloud.otherdomain.invalid + +# Shared secret for requests from and to the backend servers. This must be the +# same value as configured in the Nextcloud admin ui. +#secret = the-shared-secret + +[signaling] +# Common shared secret for authenticating as an internal client of signaling +# servers if a specific secret is not set for a signaling server. This must be +# the same value as configured in the signaling server configuration file. +#internalsecret = the-shared-secret-for-internal-clients + +# Comma-separated list of signaling servers with specific internal secrets. +#signalings = signaling-id, another-signaling + +# Signaling server configurations as defined in the "[signaling]" section above. +# The section names must match the ids used in "signalings" above. +#[signaling-id] +# URL of the signaling server +#url = https://signaling.domain.invalid + +# Shared secret for authenticating as an internal client of signaling servers. +# This must be the same value as configured in the signaling server +# configuration file. +#internalsecret = the-shared-secret-for-internal-clients + +#[another-signaling] +# URL of the signaling server +#url = https://signaling.otherdomain.invalid + +# Shared secret for authenticating as an internal client of signaling servers. +# This must be the same value as configured in the signaling server +# configuration file. +#internalsecret = the-shared-secret-for-internal-clients + +[ffmpeg] +# The ffmpeg executable (name or full path) and the global options given to +# ffmpeg. The options given here fully override the default global options. +#common = ffmpeg -loglevel level+warning -n + +# The (additional) options given to ffmpeg for the audio input. The options +# given here extend the default options for the audio input, although they do +# not override them. +# Default options: '-f pulse -i {AUDIO_SOURCE}' +#inputaudio = + +# The (additional) options given to ffmpeg for the video input. The options +# given here extend the default options for the video input, although they do +# not override them. +# Default options: '-f x11grab -draw_mouse 0 -video_size {WIDTH}x{HEIGHT} -i {VIDEO_SOURCE}' +#inputvideo = + +# The options given to ffmpeg to encode the audio output. The options given here +# fully override the default options for the audio output. +#outputaudio = -c:a libopus + +# The options given to ffmpeg to encode the video output. The options given here +# fully override the default options for the video output. +#outputvideo = -c:v libvpx -deadline:v realtime -crf 10 -b:v 1M + +# The extension of the file for audio only recordings. +#extensionaudio = .ogg + +# The extension of the file for audio and video recordings. +#extensionvideo = .webm + +[recording] +# Browser to use for recordings. Please note that the "chrome" value does not +# refer to the web browser, but to the Selenium WebDriver. In practice, "chrome" +# will use Google Chrome, or Chromium if Google Chrome is not installed. +# Allowed values: firefox, chrome +# Defaults to firefox +#browser = firefox + +# Path to the Selenium driver to use for recordings. +# If set the driver must match the browser being used (for example, +# "/usr/bin/geckodriver" for "firefox"). If no driver is explicitly set Selenium +# Manager will try to find the right one in $PATH, downloading it as a fallback. +# Note that Selenium Manager does not work in some architectures (for example, +# Linux on arm64/aarch64), so in those architectures the driver must be +# explicitly set. +#driverPath = + +# Path to the browser executable to use for recordings. +# If set the executable must match the browser being used (for example, +# "/usr/bin/firefox-esr" for "firefox"). If no executable is explicitly set +# Selenium Manager will try to find the right one in $PATH. Depending on the +# installed Selenium version if the executable is not found Selenium Manager may +# also download the browser as a fallback. +# Note that Selenium Manager does not work in some architectures (for example, +# Linux on arm64/aarch64); in those architectures the Selenium driver will try +# to find the executable, but the executable may need to be explicitly set if +# not found by the driver. +#browserPath = + +[stats] +# Comma-separated list of IP addresses (or CIDR networks) that are allowed to +# access the stats endpoint. +# Leave commented to only allow access from "127.0.0.1". +#allowed_ips = diff --git a/Containers/talk-recording/start.sh b/Containers/talk-recording/start.sh new file mode 100644 index 00000000..b49e5e9c --- /dev/null +++ b/Containers/talk-recording/start.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Variables +if [ -z "$NC_DOMAIN" ]; then + echo "You need to provide the NC_DOMAIN." + exit 1 +elif [ -z "$RECORDING_SECRET" ]; then + echo "You need to provide the RECORDING_SECRET." + exit 1 +elif [ -z "$INTERNAL_SECRET" ]; then + echo "You need to provide the INTERNAL_SECRET." + exit 1 +fi + +if [ -z "$HPB_DOMAIN" ]; then + export HPB_DOMAIN="$NC_DOMAIN" +fi + +# Delete all contents on startup to start fresh +rm -fr /tmp/{*,.*} + +cat << RECORDING_CONF > "/conf/recording.conf" +[logs] +# 30 means Warning +level = 30 + +[http] +listen = 0.0.0.0:1234 + +[backend] +allowall = ${ALLOW_ALL} +# The secret below is still needed if allowall is set to true, also it doesn't hurt to be here +secret = ${RECORDING_SECRET} +backends = backend-1 +skipverify = ${SKIP_VERIFY} +maxmessagesize = 1024 +videowidth = 1920 +videoheight = 1080 +directory = /tmp + +[backend-1] +url = ${NC_PROTOCOL}://${NC_DOMAIN} +secret = ${RECORDING_SECRET} +skipverify = ${SKIP_VERIFY} + +[signaling] +signalings = signaling-1 + +[signaling-1] +url = ${HPB_PROTOCOL}://${HPB_DOMAIN}${HPB_PATH} +internalsecret = ${INTERNAL_SECRET} + +[ffmpeg] +# common = ffmpeg -loglevel level+warning -n +# outputaudio = -c:a libopus +# outputvideo = -c:v libvpx -deadline:v realtime -crf 10 -b:v 1M +extensionaudio = .ogg +extensionvideo = .webm + +[recording] +browser = firefox +driverPath = /usr/bin/geckodriver +browserPath = /usr/bin/firefox +RECORDING_CONF + +exec "$@" diff --git a/Containers/talk/Dockerfile b/Containers/talk/Dockerfile index 6b666374..e8d3d72f 100644 --- a/Containers/talk/Dockerfile +++ b/Containers/talk/Dockerfile @@ -1,72 +1,111 @@ -FROM ubuntu:focal-20220801 +# syntax=docker/dockerfile:latest +FROM nats:2.12.4-scratch AS nats +FROM eturnal/eturnal:1.12.2-alpine AS eturnal +FROM strukturag/nextcloud-spreed-signaling:2.0.4 AS signaling +FROM alpine:3.23.3 AS janus +ARG JANUS_VERSION=v1.3.3 +WORKDIR /src RUN set -ex; \ - \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - openssl \ - coturn \ - supervisor \ - curl \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ ca-certificates \ - netcat \ - ; \ - rm -rf /var/lib/apt/lists/* - -RUN set -ex; \ - curl -sL -o "/etc/apt/trusted.gpg.d/morph027-nats-server.asc" "https://packaging.gitlab.io/nats-server/gpg.key"; \ - echo "deb https://packaging.gitlab.io/nats-server nats main" > /etc/apt/sources.list.d/morph027-nats-server.list; \ - . /etc/lsb-release; \ - curl -sL -o "/etc/apt/trusted.gpg.d/morph027-janus.asc" "https://packaging.gitlab.io/janus/gpg.key"; \ - echo "deb https://packaging.gitlab.io/janus/$DISTRIB_CODENAME $DISTRIB_CODENAME main" > /etc/apt/sources.list.d/morph027-janus.list; \ - curl -sL -o "/etc/apt/trusted.gpg.d/morph027-nextcloud-spreed-signaling.asc" "https://packaging.gitlab.io/nextcloud-spreed-signaling/gpg.key"; \ - echo "deb https://packaging.gitlab.io/nextcloud-spreed-signaling signaling main" > /etc/apt/sources.list.d/morph027-nextcloud-spreed-signaling.list + git \ + autoconf \ + automake \ + build-base \ + pkgconfig \ + libtool \ + util-linux \ + glib-dev \ + zlib-dev \ + openssl-dev \ + jansson-dev \ + libnice-dev \ + libconfig-dev \ + libsrtp-dev \ + libusrsctp-dev \ + gengetopt-dev \ + libwebsockets-dev; \ + git clone --recursive https://github.com/meetecho/janus-gateway --depth=1 --single-branch --branch "$JANUS_VERSION" /src; \ + /src/autogen.sh; \ + /src/configure --disable-rabbitmq --disable-mqtt --disable-boringssl; \ + make; \ + make install; \ + make configs; \ + rename -v ".jcfg.sample" ".jcfg" /usr/local/etc/janus/*.jcfg.sample + +FROM alpine:3.23.3 +ENV ETURNAL_ETC_DIR="/conf" +ENV SKIP_CERT_VERIFY=false +COPY --from=janus --chmod=777 --chown=1000:1000 /usr/local /usr/local +COPY --from=eturnal --chmod=777 --chown=1000:1000 /opt/eturnal /opt/eturnal +COPY --from=nats --chmod=777 --chown=1000:1000 /nats-server /usr/local/bin/nats-server +COPY --from=signaling --chmod=777 --chown=1000:1000 /usr/bin/nextcloud-spreed-signaling /usr/local/bin/nextcloud-spreed-signaling + +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh +COPY --chmod=664 supervisord.conf /supervisord.conf RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + ca-certificates \ + tzdata \ + bash \ + openssl \ + supervisor \ + bind-tools \ + netcat-openbsd \ + \ + glib \ + zlib \ + libssl3 \ + libcrypto3 \ + jansson \ + libnice \ + libconfig \ + libsrtp \ + libusrsctp \ + libwebsockets \ + \ + shadow \ + grep; \ + useradd --system -u 1000 eturnal; \ + apk del --no-cache \ + shadow; \ \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - nats-server \ - janus \ - nextcloud-spreed-signaling \ - ; \ - rm -rf /var/lib/apt/lists/* - -RUN adduser --system --group talk - -RUN mkdir /var/log/supervisord; \ - mkdir /var/run/supervisord; \ - chown talk:talk /var/run/supervisord; \ - chown talk:talk /var/log/supervisord; - -COPY start.sh /usr/bin/ -COPY supervisord.conf / -RUN chmod +x /usr/bin/start.sh; \ - chmod +r /supervisord.conf; \ - touch /etc/turnserver.conf; \ - chown talk:talk /etc/turnserver.conf; \ - sed -i '/TURNSERVER_ENABLED/c\TURNSERVER_ENABLED=1' /etc/default/coturn; \ - mkdir -p /var/tmp; - -RUN curl -sL -o "/usr/share/janus/lua/json.lua" "https://raw.githubusercontent.com/rxi/json.lua/master/json.lua"; \ - curl -sL -o "/usr/share/janus/lua/ansicolors.lua" "https://raw.githubusercontent.com/kikito/ansicolors.lua/master/ansicolors.lua" - -RUN mkdir -p /etc/nats; \ - echo "listen: 127.0.0.1:4222" > /etc/nats/nats.conf; \ - chown talk:talk /etc; \ - chown talk:talk -R /etc/nats; \ - chown talk:talk -R /etc/janus; \ - chown talk:talk -R /etc/signaling; \ - chown talk:talk -R /usr - # Give root a random password -RUN echo "root:$(openssl rand -base64 12)" | chpasswd + echo "root:$(openssl rand -base64 12)" | chpasswd; \ + \ + touch \ + /etc/nats.conf \ + /etc/eturnal.yml; \ + echo "listen: 127.0.0.1:4222" | tee /etc/nats.conf; \ + mkdir -p \ + /var/tmp \ + /conf \ + /var/lib/turn \ + /var/log/supervisord \ + /var/run/supervisord \ + /usr/local/lib/janus/loggers; \ + chown eturnal:eturnal -R \ + /etc/nats.conf \ + /var/log/supervisord \ + /var/run/supervisord; \ + chmod 777 -R \ + /tmp \ + /conf \ + /var/run/supervisord \ + /var/log/supervisord; \ + ln -s /opt/eturnal/bin/stun /usr/local/bin/stun; \ + ln -s /opt/eturnal/bin/eturnalctl /usr/local/bin/eturnalctl -# Set default talk port https://github.com/nextcloud/all-in-one/issues/1011 -ENV TALK_PORT=3478 +USER 1000 +ENTRYPOINT ["/start.sh"] +CMD ["supervisord", "-c", "/supervisord.conf"] -USER talk -ENTRYPOINT ["start.sh"] -CMD ["/usr/bin/supervisord", "-c", "/supervisord.conf"] - -HEALTHCHECK CMD (curl -skI localhost:8081 && curl -skI localhost:8188 && curl -skf --http0.9 localhost:4222 && nc -z localhost $TALK_PORT) || exit 1 \ No newline at end of file +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/talk/healthcheck.sh b/Containers/talk/healthcheck.sh new file mode 100644 index 00000000..e4544763 --- /dev/null +++ b/Containers/talk/healthcheck.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +nc -z 127.0.0.1 8081 || exit 1 +nc -z 127.0.0.1 8188 || exit 1 +nc -z 127.0.0.1 4222 || exit 1 +nc -z 127.0.0.1 "$TALK_PORT" || exit 1 +eturnalctl status || exit 1 diff --git a/Containers/talk/server.conf.in b/Containers/talk/server.conf.in new file mode 100644 index 00000000..8f437e30 --- /dev/null +++ b/Containers/talk/server.conf.in @@ -0,0 +1,342 @@ +[http] +# IP and port to listen on for HTTP requests. +# Comment line to disable the listener. +#listen = 127.0.0.1:8080 + +# HTTP socket read timeout in seconds. +#readtimeout = 15 + +# HTTP socket write timeout in seconds. +#writetimeout = 30 + +[https] +# IP and port to listen on for HTTPS requests. +# Comment line to disable the listener. +#listen = 127.0.0.1:8443 + +# HTTPS socket read timeout in seconds. +#readtimeout = 15 + +# HTTPS socket write timeout in seconds. +#writetimeout = 30 + +# Certificate / private key to use for the HTTPS server. +certificate = /etc/nginx/ssl/server.crt +key = /etc/nginx/ssl/server.key + +[app] +# Set to "true" to install pprof debug handlers. +# See "https://golang.org/pkg/net/http/pprof/" for further information. +debug = false + +# Set to "true" to allow subscribing any streams. This is insecure and should +# only be enabled for testing. By default only streams of users in the same +# room and call can be subscribed. +#allowsubscribeany = false + +# Comma separated list of trusted proxies (IPs or CIDR networks) that may set +# the "X-Real-Ip" or "X-Forwarded-For" headers. If both are provided, the +# "X-Real-Ip" header will take precedence (if valid). +# Leave empty to allow loopback and local addresses. +#trustedproxies = + +[sessions] +# Secret value used to generate checksums of sessions. This should be a random +# string of 32 or 64 bytes. +hashkey = the-secret-for-session-checksums + +# Optional key for encrypting data in the sessions. Must be either 16, 24 or +# 32 bytes. +# If no key is specified, data will not be encrypted (not recommended). +blockkey = -encryption-key- + +[clients] +# Shared secret for connections from internal clients. This must be the same +# value as configured in the respective internal services. +internalsecret = the-shared-secret-for-internal-clients + +[federation] +# If set to "true", certificate validation of federation targets will be skipped. +# This should only be enabled during development, e.g. to work with self-signed +# certificates. +#skipverify = false + +# Timeout in seconds for requests to federation targets. +#timeout = 10 + +[backend] +# Type of backend configuration. +# Defaults to "static". +# +# Possible values: +# - static: A comma-separated list of backends is given in the "backends" option. +# - etcd: Backends are retrieved from an etcd cluster. +#backendtype = static + +# For backend type "static": +# Comma-separated list of backend ids from which clients are allowed to connect +# from. Each backend will have isolated rooms, i.e. clients connecting to room +# "abc12345" on backend 1 will be in a different room than clients connected to +# a room with the same name on backend 2. Also sessions connected from different +# backends will not be able to communicate with each other. +#backends = backend-id, another-backend + +# For backend type "etcd": +# Key prefix of backend entries. All keys below will be watched and assumed to +# contain a JSON document with the following entries: +# - "urls": List of urls of the Nextcloud instance. +# - "url": Url of the Nextcloud instance (deprecated). +# - "secret": Shared secret for requests from and to the backend servers. +# +# Additional optional entries: +# - "maxstreambitrate": Maximum bitrate per publishing stream (in bits per second). +# - "maxscreenbitrate": Maximum bitrate per screensharing stream (in bits per second). +# - "sessionlimit": Number of sessions that are allowed to connect. +# +# Example: +# "/signaling/backend/one" -> {"urls": ["https://nextcloud.domain1.invalid"], ...} +# "/signaling/backend/two" -> {"urls": ["https://domain2.invalid/nextcloud"], ...} +#backendprefix = /signaling/backend + +# Allow any hostname as backend endpoint. This is extremely insecure and should +# only be used while running the benchmark client against the server. +allowall = false + +# Common shared secret for requests from and to the backend servers. Used if +# "allowall" is enabled or as fallback for individual backends that don't have +# their own secret set. +# This must be the same value as configured in the Nextcloud admin ui. +#secret = the-shared-secret-for-allowall + +# Timeout in seconds for requests to the backend. +timeout = 10 + +# Maximum number of concurrent backend connections per host. +connectionsperhost = 8 + +# If set to "true", certificate validation of backend endpoints will be skipped. +# This should only be enabled during development, e.g. to work with self-signed +# certificates. +#skipverify = false + +# For backendtype "static": +# Backend configurations as defined in the "[backend]" section above. The +# section names must match the ids used in "backends" above. +#[backend-id] +# Comma-separated list of urls of the Nextcloud instance +#urls = https://cloud.domain.invalid + +# Shared secret for requests from and to the backend servers. Leave empty to use +# the common shared secret from above. +# This must be the same value as configured in the Nextcloud admin ui. +#secret = the-shared-secret + +# Limit the number of sessions that are allowed to connect to this backend. +# Omit or set to 0 to not limit the number of sessions. +#sessionlimit = 10 + +# The maximum bitrate per publishing stream (in bits per second). +# Defaults to the maximum bitrate configured for the proxy / MCU. +#maxstreambitrate = 1048576 + +# The maximum bitrate per screensharing stream (in bits per second). +# Defaults to the maximum bitrate configured for the proxy / MCU. +#maxscreenbitrate = 2097152 + +#[another-backend] +# Comma-separated list of urls of the Nextcloud instance +#urls = https://cloud.otherdomain.invalid + +# Shared secret for requests from and to the backend servers. Leave empty to use +# the common shared secret from above. +# This must be the same value as configured in the Nextcloud admin ui. +#secret = the-shared-secret + +[nats] +# Url of NATS backend to use. This can also be a list of URLs to connect to +# multiple backends. For local development, this can be set to "nats://loopback" +# to process NATS messages internally instead of sending them through an +# external NATS backend. +#url = nats://localhost:4222 + +[mcu] +# The type of the MCU to use. Currently only "janus" and "proxy" are supported. +# Leave empty to disable MCU functionality. +#type = + +# For type "janus": the URL to the websocket endpoint of the MCU server. +# For type "proxy": a space-separated list of proxy URLs to connect to. +#url = + +# The maximum bitrate per publishing stream (in bits per second). +# Defaults to 1 mbit/sec. +# For type "proxy": will be capped to the maximum bitrate configured at the +# proxy server that is used. +#maxstreambitrate = 1048576 + +# The maximum bitrate per screensharing stream (in bits per second). +# Default is 2 mbit/sec. +# For type "proxy": will be capped to the maximum bitrate configured at the +# proxy server that is used. +#maxscreenbitrate = 2097152 + +# List of IP addresses / subnets that are allowed to be used by clients in +# candidates. The allowed list has preference over the blocked list below. +#allowedcandidates = 10.0.0.0/8 + +# List of IP addresses / subnets to filter from candidates received by clients. +#blockedcandidates = 1.2.3.0/24 + +# For type "proxy": timeout in seconds for requests to the proxy server. +#proxytimeout = 2 + +# For type "proxy": type of URL configuration for proxy servers. +# Defaults to "static". +# +# Possible values: +# - static: A space-separated list of proxy URLs is given in the "url" option. +# - etcd: Proxy URLs are retrieved from an etcd cluster (see below). +#urltype = static + +# If set to "true", certificate validation of proxy servers will be skipped. +# This should only be enabled during development, e.g. to work with self-signed +# certificates. +#skipverify = false + +# For type "proxy": the id of the token to use when connecting to proxy servers. +#token_id = server1 + +# For type "proxy": the private key for the configured token id to use when +# connecting to proxy servers. +#token_key = privkey.pem + +# For url type "static": Enable DNS discovery on hostname of configured URL. +# If the hostname resolves to multiple IP addresses, a connection is established +# to each of them. +# Changes to the DNS are monitored regularly and proxy connections are created +# or deleted as necessary. +#dnsdiscovery = true + +# For url type "etcd": Key prefix of MCU proxy entries. All keys below will be +# watched and assumed to contain a JSON document. The entry "address" from this +# document will be used as proxy URL, other contents in the document will be +# ignored. +# +# Example: +# "/signaling/proxy/server/one" -> {"address": "https://proxy1.domain.invalid"} +# "/signaling/proxy/server/two" -> {"address": "https://proxy2.domain.invalid"} +#keyprefix = /signaling/proxy/server + +[turn] +# API key that the MCU will need to send when requesting TURN credentials. +#apikey = the-api-key-for-the-rest-service + +# The shared secret to use for generating TURN credentials. This must be the +# same as on the TURN server. +#secret = 6d1c17a7-c736-4e22-b02c-e2955b7ecc64 + +# A comma-separated list of TURN servers to use. Leave empty to disable the +# TURN REST API. +#servers = turn:1.2.3.4:9991?transport=udp,turn:1.2.3.4:9991?transport=tcp + +[geoip] +# License key to use when downloading the MaxMind GeoIP database. You can +# register an account at "https://www.maxmind.com/en/geolite2/signup" for +# free. See "https://dev.maxmind.com/geoip/geoip2/geolite2/" for further +# information. +# You can also get a free GeoIP database from https://db-ip.com/ without +# registration. Provide the URL below in this case. +# Leave empty to disable GeoIP lookups. +#license = + +# Optional URL to download a MaxMind GeoIP database from. Will be generated if +# "license" is provided above. Can be a "file://" url if a local file should +# be used. Please note that the database must provide a country field when +# looking up IP addresses. +#url = + +[geoip-overrides] +# Optional overrides for GeoIP lookups. The key is an IP address / range, the +# value the associated country code. +#127.0.0.1 = DE +#192.168.0.0/24 = DE + +[continent-overrides] +# Optional overrides for continent mappings. The key is a continent code, the +# value a comma-separated list of continent codes to map the continent to. +# Use European servers for clients in Africa. +#AF = EU +# Use servers in North Africa for clients in South America. +#SA = NA + +[stats] +# Comma-separated list of IP addresses that are allowed to access the stats +# endpoint. Leave empty (or commented) to only allow access from "127.0.0.1". +#allowed_ips = + +[etcd] +# Comma-separated list of static etcd endpoints to connect to. +#endpoints = 127.0.0.1:2379,127.0.0.1:22379,127.0.0.1:32379 + +# Options to perform endpoint discovery through DNS SRV. +# Only used if no endpoints are configured manually. +#discoverysrv = example.com +#discoveryservice = foo + +# Path to private key, client certificate and CA certificate if TLS +# authentication should be used. +#clientkey = /path/to/etcd-client.key +#clientcert = /path/to/etcd-client.crt +#cacert = /path/to/etcd-ca.crt + +[grpc] +# IP and port to listen on for GRPC requests. +# Comment line to disable the listener. +#listen = 0.0.0.0:9090 + +# Certificate / private key to use for the GRPC server. +# Omit to use unencrypted connections. +#servercertificate = /path/to/grpc-server.crt +#serverkey = /path/to/grpc-server.key + +# CA certificate that is allowed to issue certificates of GRPC servers. +# Omit to expect unencrypted connections. +#serverca = /path/to/grpc-ca.crt + +# Certificate / private key to use for the GRPC client. +# Omit if clients don't need to authenticate on the server. +#clientcertificate = /path/to/grpc-client.crt +#clientkey = /path/to/grpc-client.key + +# CA certificate that is allowed to issue certificates of GRPC clients. +# Omit to allow any clients to connect. +#clientca = /path/to/grpc-ca.crt + +# Type of GRPC target configuration. +# Defaults to "static". +# +# Possible values: +# - static: A comma-separated list of targets is given in the "targets" option. +# - etcd: Target URLs are retrieved from an etcd cluster. +#targettype = static + +# For target type "static": Comma-separated list of GRPC targets to connect to +# for clustering mode. +#targets = 192.168.0.1:9090, 192.168.0.2:9090 + +# For target type "static": Enable DNS discovery on hostnames of GRPC target. +# If a hostname resolves to multiple IP addresses, a connection is established +# to each of them. +# Changes to the DNS are monitored regularly and GRPC clients are created or +# deleted as necessary. +#dnsdiscovery = true + +# For target type "etcd": Key prefix of GRPC target entries. All keys below will +# be watched and assumed to contain a JSON document. The entry "address" from +# this document will be used as target URL, other contents in the document will +# be ignored. +# +# Example: +# "/signaling/cluster/grpc/one" -> {"address": "192.168.0.1:9090"} +# "/signaling/cluster/grpc/two" -> {"address": "192.168.0.2:9090"} +#targetprefix = /signaling/cluster/grpc diff --git a/Containers/talk/start.sh b/Containers/talk/start.sh index eea4563d..f89949f3 100644 --- a/Containers/talk/start.sh +++ b/Containers/talk/start.sh @@ -4,49 +4,79 @@ if [ -z "$NC_DOMAIN" ]; then echo "You need to provide the NC_DOMAIN." exit 1 +elif [ -z "$TALK_PORT" ]; then + echo "You need to provide the TALK_PORT." + exit 1 elif [ -z "$TURN_SECRET" ]; then echo "You need to provide the TURN_SECRET." exit 1 -elif [ -z "$JANUS_API_KEY" ]; then - echo "You need to provide the JANUS_API_KEY." - exit 1 elif [ -z "$SIGNALING_SECRET" ]; then echo "You need to provide the SIGNALING_SECRET." exit 1 +elif [ -z "$INTERNAL_SECRET" ]; then + echo "You need to provide the INTERNAL_SECRET." + exit 1 fi -# Turn -cat << TURN_CONF > "/etc/turnserver.conf" -listening-port=$TALK_PORT -fingerprint -lt-cred-mech -use-auth-secret -static-auth-secret=$TURN_SECRET -realm=$NC_DOMAIN -total-quota=100 -bps-capacity=0 -stale-nonce -no-multicast-peers -simple-log -pidfile=/var/tmp/turnserver.pid -TURN_CONF - -# Janus set -x -sed -i "s|#turn_rest_api_key.*|turn_rest_api_key = \"$JANUS_API_KEY\"|" /etc/janus/janus.jcfg -sed -i "s|#full_trickle.*|full_trickle = true|g" /etc/janus/janus.jcfg -sed -i 's|#stun_server.*|stun_server = "127.0.0.1"|g' /etc/janus/janus.jcfg -sed -i "s|#stun_port.*|stun_port = $TALK_PORT|g" /etc/janus/janus.jcfg -sed -i "s|#turn_port.*|turn_port = $TALK_PORT|g" /etc/janus/janus.jcfg -sed -i 's|#turn_server.*|turn_server = "127.0.0.1"|g' /etc/janus/janus.jcfg -sed -i 's|#turn_type .*|turn_type = "udp"|g' /etc/janus/janus.jcfg -sed -i 's|#ice_ignore_list .*|ice_ignore_list = "udp"|g' /etc/janus/janus.jcfg -sed -i 's|#interface.*|interface = "lo"|g' /etc/janus/janus.transport.websockets.jcfg -sed -i 's|#ws_interface.*|ws_interface = "lo"|g' /etc/janus/janus.transport.websockets.jcfg +IPv4_ADDRESS_TALK_RELAY="$(hostname -i | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)" +# shellcheck disable=SC2153 +IPv4_ADDRESS_TALK="$(dig "$TALK_HOST" IN A +short +search | grep '^[0-9.]\+$' | sort | head -n1)" +# shellcheck disable=SC2153 +IPv6_ADDRESS_TALK="$(dig "$TALK_HOST" AAAA +short +search | grep '^[0-9a-f:]\+$' | sort | head -n1)" set +x +if [ -n "$IPv4_ADDRESS_TALK" ] && [ "$IPv4_ADDRESS_TALK_RELAY" = "$IPv4_ADDRESS_TALK" ]; then + IPv4_ADDRESS_TALK="" +fi + +set -x +IP_BINDING="::" +if grep -q "1" /sys/module/ipv6/parameters/disable \ +|| grep -q "1" /proc/sys/net/ipv6/conf/all/disable_ipv6 \ +|| grep -q "1" /proc/sys/net/ipv6/conf/default/disable_ipv6; then + IP_BINDING="0.0.0.0" +fi +set +x + +# Turn +cat << TURN_CONF > "/conf/eturnal.yml" +eturnal: + listen: + - ip: "$IP_BINDING" + port: $TALK_PORT + transport: udp + - ip: "$IP_BINDING" + port: $TALK_PORT + transport: tcp + log_dir: stdout + log_level: warning + secret: "$TURN_SECRET" + relay_ipv4_addr: "$IPv4_ADDRESS_TALK_RELAY" + relay_ipv6_addr: "$IPv6_ADDRESS_TALK" + blacklist_peers: + - recommended + whitelist_peers: + - 127.0.0.1 + - ::1 + - "$IPv4_ADDRESS_TALK_RELAY" + - "$IPv4_ADDRESS_TALK" + - "$IPv6_ADDRESS_TALK" +TURN_CONF + +# Remove empty lines so that the config is not invalid +sed -i '/""/d' /conf/eturnal.yml + +if [ -z "$TALK_MAX_STREAM_BITRATE" ]; then + TALK_MAX_STREAM_BITRATE=1048576 +fi + +if [ -z "$TALK_MAX_SCREEN_BITRATE" ]; then + TALK_MAX_SCREEN_BITRATE=2097152 +fi + # Signling -cat << SIGNALING_CONF > "/etc/signaling/server.conf" +cat << SIGNALING_CONF > "/conf/signaling.conf" [http] listen = 0.0.0.0:8081 @@ -58,17 +88,20 @@ hashkey = $(openssl rand -hex 16) blockkey = $(openssl rand -hex 16) [clients] -internalsecret = $(openssl rand -hex 16) +internalsecret = ${INTERNAL_SECRET} [backend] backends = backend-1 allowall = false timeout = 10 connectionsperhost = 8 +skipverify = ${SKIP_CERT_VERIFY} [backend-1] -url = https://${NC_DOMAIN} +urls = https://${NC_DOMAIN} secret = ${SIGNALING_SECRET} +maxstreambitrate = ${TALK_MAX_STREAM_BITRATE} +maxscreenbitrate = ${TALK_MAX_SCREEN_BITRATE} [nats] url = nats://127.0.0.1:4222 @@ -76,11 +109,8 @@ url = nats://127.0.0.1:4222 [mcu] type = janus url = ws://127.0.0.1:8188 - -[turn] -apikey = ${JANUS_API_KEY} -secret = ${TURN_SECRET} -servers = turn:$NC_DOMAIN:$TALK_PORT?transport=tcp,turn:$NC_DOMAIN:$TALK_PORT?transport=udp +maxstreambitrate = ${TALK_MAX_STREAM_BITRATE} +maxscreenbitrate = ${TALK_MAX_SCREEN_BITRATE} SIGNALING_CONF exec "$@" diff --git a/Containers/talk/supervisord.conf b/Containers/talk/supervisord.conf index 5e2dcd79..89287db5 100644 --- a/Containers/talk/supervisord.conf +++ b/Containers/talk/supervisord.conf @@ -1,6 +1,5 @@ [supervisord] nodaemon=true -nodaemon=true logfile=/var/log/supervisord/supervisord.log pidfile=/var/run/supervisord/supervisord.pid childlogdir=/var/log/supervisord/ @@ -8,30 +7,31 @@ logfile_maxbytes=50MB logfile_backups=10 loglevel=error -[program:turnserver] +[program:eturnal] stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -command=turnserver +command=eturnalctl foreground [program:nats-server] stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -command=nats-server -c /etc/nats/nats.conf +command=nats-server -c /etc/nats.conf [program:janus] stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -command=/usr/bin/janus --config=/etc/janus/janus.jcfg --disable-colors --daemon --log-stdout +# debug-level 3 means warning +command=janus --config=/usr/local/etc/janus/janus.jcfg --disable-colors --log-stdout --full-trickle --debug-level 3 [program:signaling] stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -command=signaling -config /etc/signaling/server.conf +command=nextcloud-spreed-signaling -config /conf/signaling.conf diff --git a/Containers/watchtower/Dockerfile b/Containers/watchtower/Dockerfile index 7a54bbf2..fc9ea093 100644 --- a/Containers/watchtower/Dockerfile +++ b/Containers/watchtower/Dockerfile @@ -1,13 +1,28 @@ -# From https://github.com/containrrr/watchtower/blob/main/dockerfiles/Dockerfile.self-contained -FROM containrrr/watchtower:1.4.0 as watchtower +# syntax=docker/dockerfile:latest +FROM golang:1.25.6-alpine3.23 AS go -FROM alpine:3.16.2 +ENV WATCHTOWER_COMMIT_HASH=f522ce27e1fbe4618da54833025a95be62aa838a -RUN apk add --update --no-cache bash -COPY --from=watchtower /watchtower / +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + build-base; \ + go install github.com/nicholas-fedor/watchtower@$WATCHTOWER_COMMIT_HASH # v1.14.0 -COPY start.sh / -RUN chmod +x /start.sh +FROM alpine:3.23.3 +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache bash ca-certificates tzdata + +COPY --from=go /go/bin/watchtower /watchtower + +COPY --chmod=775 start.sh /start.sh + +# hadolint ignore=DL3002 USER root + ENTRYPOINT ["/start.sh"] +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/watchtower/start.sh b/Containers/watchtower/start.sh index 1f6e3097..cf16e7a4 100644 --- a/Containers/watchtower/start.sh +++ b/Containers/watchtower/start.sh @@ -1,7 +1,7 @@ #!/bin/bash # Check if socket is available and readable -if ! [ -a "/var/run/docker.sock" ]; then +if ! [ -e "/var/run/docker.sock" ]; then echo "Docker socket is not available. Cannot continue." exit 1 elif ! test -r /var/run/docker.sock; then @@ -9,8 +9,15 @@ elif ! test -r /var/run/docker.sock; then exit 1 fi +if [ -f /run/.containerenv ]; then + # If running under podman disable memory_swappiness setting in watchtower. + # It is a necessary workaround until https://github.com/containers/podman/issues/23824 gets fixed. + echo "Running under Podman. Setting WATCHTOWER_DISABLE_MEMORY_SWAPPINESS to 1." + export WATCHTOWER_DISABLE_MEMORY_SWAPPINESS=1 +fi + if [ -n "$CONTAINER_TO_UPDATE" ]; then - exec /watchtower --cleanup --run-once "$CONTAINER_TO_UPDATE" + exec /watchtower --cleanup --debug --run-once "$CONTAINER_TO_UPDATE" else echo "'CONTAINER_TO_UPDATE' is not set. Cannot update anything." exit 1 diff --git a/Containers/whiteboard/Dockerfile b/Containers/whiteboard/Dockerfile new file mode 100644 index 00000000..c83dd46b --- /dev/null +++ b/Containers/whiteboard/Dockerfile @@ -0,0 +1,27 @@ +# syntax=docker/dockerfile:latest +# Probably from this file: https://github.com/nextcloud/whiteboard/blob/main/Dockerfile +FROM ghcr.io/nextcloud-releases/whiteboard:v1.5.4 + +USER root +RUN set -ex; \ + apk add --no-cache bash jq; \ + chmod 777 -R /tmp; \ + if [ -f /usr/lib/chromium/chrome_crashpad_handler ] && [ ! -f /usr/lib/chromium/chrome_crashpad_handler.real ]; then \ + mv /usr/lib/chromium/chrome_crashpad_handler /usr/lib/chromium/chrome_crashpad_handler.real; \ + printf '%s\n' '#!/bin/sh' "exec /usr/lib/chromium/chrome_crashpad_handler.real --no-periodic-tasks --database=\"\${CRASHPAD_DATABASE:-/tmp/chrome-crashpad}\" \"\$@\"" >/usr/lib/chromium/chrome_crashpad_handler; \ + chmod +x /usr/lib/chromium/chrome_crashpad_handler; \ + fi +USER 65534 + +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +HEALTHCHECK CMD /healthcheck.sh + +WORKDIR /tmp + +ENTRYPOINT ["/start.sh"] + +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/Containers/whiteboard/healthcheck.sh b/Containers/whiteboard/healthcheck.sh new file mode 100644 index 00000000..5909db82 --- /dev/null +++ b/Containers/whiteboard/healthcheck.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +nc -z "$REDIS_HOST" "$REDIS_PORT" || exit 0 +nc -z 127.0.0.1 3002 || exit 1 diff --git a/Containers/whiteboard/start.sh b/Containers/whiteboard/start.sh new file mode 100644 index 00000000..e0babd7f --- /dev/null +++ b/Containers/whiteboard/start.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Only start container if nextcloud is accessible +while ! nc -z "$REDIS_HOST" "$REDIS_PORT"; do + echo "Waiting for redis to start..." + sleep 5 +done + +# Set a default for redis db index +if [ -z "$REDIS_DB_INDEX" ]; then + REDIS_DB_INDEX=0 +fi + +# URL-encode password +REDIS_HOST_PASSWORD="$(jq -rn --arg v "$REDIS_HOST_PASSWORD" '$v|@uri')" + +export REDIS_URL="redis://$REDIS_USER:$REDIS_HOST_PASSWORD@$REDIS_HOST:$REDIS_PORT/$REDIS_DB_INDEX" + +# Run it +exec npm --prefix /app run server:start diff --git a/app/appinfo/info.xml b/app/appinfo/info.xml index 1f9c05c8..832d3ccd 100644 --- a/app/appinfo/info.xml +++ b/app/appinfo/info.xml @@ -2,10 +2,10 @@ nextcloud-aio - Nextcloud All In One + Nextcloud All-in-One Provides a login link for admins. - Add a link to the admin settings that gives access to the Nextcloud All In One admin interface - 0.2.0 + Add a link to the admin settings that gives access to the Nextcloud All-in-One admin interface + 0.8.0 agpl Azul AllInOne @@ -13,20 +13,11 @@ monitoring https://github.com/nextcloud/all-in-one/issues - + OCA\AllInOne\Settings\Admin - - diff --git a/app/lib/Settings/Admin.php b/app/lib/Settings/Admin.php index 2d5212ae..36fbd01b 100644 --- a/app/lib/Settings/Admin.php +++ b/app/lib/Settings/Admin.php @@ -78,6 +78,6 @@ class Admin implements ISettings { * E.g.: 70 */ public function getPriority(): int { - return 5; + return 0; } } diff --git a/app/templates/admin.php b/app/templates/admin.php index 64064f5d..4812ad90 100644 --- a/app/templates/admin.php +++ b/app/templates/admin.php @@ -10,6 +10,7 @@ declare(strict_types=1); */ /** @var array $_ */ ?> diff --git a/community-containers/borgbackup-viewer/borgbackup-viewer.json b/community-containers/borgbackup-viewer/borgbackup-viewer.json new file mode 100644 index 00000000..7f9bb0a0 --- /dev/null +++ b/community-containers/borgbackup-viewer/borgbackup-viewer.json @@ -0,0 +1,71 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-borgbackup-viewer", + "image_tag": "v1", + "display_name": "Borg Backup Viewer", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/borgbackup-viewer", + "image": "ghcr.io/szaimen/aio-borgbackup-viewer", + "internal_port": "5801", + "ports": [ + { + "ip_binding": "", + "port_number": "5801", + "protocol": "tcp" + } + ], + "environment": [ + "BORG_HOST_ID=nextcloud-aio-borgbackup-viewer", + "WEB_AUTHENTICATION_USERNAME=nextcloud", + "WEB_AUTHENTICATION_PASSWORD=%BORGBACKUP_VIEWER_PASSWORD%", + "WEB_LISTENING_PORT=5801", + "BORG_PASSPHRASE=%BORGBACKUP_PASSWORD%", + "BORG_REPO=/mnt/borgbackup/borg" + ], + "secrets": [ + "BORGBACKUP_VIEWER_PASSWORD", + "BORGBACKUP_PASSWORD" + ], + "ui_secret": "BORGBACKUP_VIEWER_PASSWORD", + "volumes": [ + { + "source": "nextcloud_aio_backup_cache", + "destination": "/root", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data", + "writeable": true + }, + { + "source": "nextcloud_aio_mastercontainer", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer", + "writeable": true + }, + { + "source": "%BORGBACKUP_HOST_LOCATION%", + "destination": "/mnt/borgbackup", + "writeable": true + }, + { + "source": "nextcloud_aio_elasticsearch", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_elasticsearch", + "writeable": true + }, + { + "source": "nextcloud_aio_redis", + "destination": "/mnt/redis", + "writeable": true + } + ], + "devices": [ + "/dev/fuse" + ], + "cap_add": [ + "SYS_ADMIN" + ], + "apparmor_unconfined": true + } + ] +} diff --git a/community-containers/borgbackup-viewer/readme.md b/community-containers/borgbackup-viewer/readme.md new file mode 100644 index 00000000..ddd11be7 --- /dev/null +++ b/community-containers/borgbackup-viewer/readme.md @@ -0,0 +1,17 @@ +## Borgbackup Viewer +This container allows to view the local borg repository in a web session. It also allows you to restore files and folders from the backup by using desktop programs in a web browser. + +### Notes +- After adding and starting the container, you need to visit `https://ip.address.of.this.server:5801` in order to log in with the user `nextcloud` and the password that you can see next to the container in the AIO interface. (The web page uses a self-signed certificate, so you need to accept the warning). +- Then, you should see a terminal. There type in `borg mount /mnt/borgbackup/borg /tmp/borg` to mount the backup archive at `/tmp/borg` inside the container. Afterwards type in `nautilus /tmp/borg` which will show a file explorer and allows you to see all the files. You can then copy files and folders back to their initial mountpoints inside `/nextcloud_aio_volumes/`, `/host_mounts/` and `/docker_volumes/`. ⚠️ Be very carefully while doing that as can break your instance! +- After you are done with the operation, click on the terminal in the background and press `[CTRL]+[c]` multiple times to close any open application. Then run `umount /tmp/borg` to unmount the mountpoint correctly. +- You can also delete specific archives by running `borg list`, delete a specific archive e.g. via `borg delete --stats --progress "::20220223_174237-nextcloud-aio"` and compact the archives via `borg compact`. After doing so, make sure to update the backup archives list in the AIO interface! You can do so by clicking on the `Update backup list` button in the `Update backup list` section inside the `Backup and restore` section. +- ⚠️ After you are done doing your operations, remove the container for better security again from the stack: https://github.com/nextcloud/all-in-one/tree/main/community-containers#how-to-remove-containers-from-aios-stack +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-borgbackup-viewer + +### Maintainer +https://github.com/szaimen + diff --git a/community-containers/caddy/caddy.json b/community-containers/caddy/caddy.json new file mode 100644 index 00000000..e27df683 --- /dev/null +++ b/community-containers/caddy/caddy.json @@ -0,0 +1,54 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-caddy", + "display_name": "Caddy with geoblocking", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy", + "image": "ghcr.io/szaimen/aio-caddy", + "image_tag": "v4", + "internal_port": "443", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "443", + "protocol": "tcp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "NC_DOMAIN=%NC_DOMAIN%", + "APACHE_PORT=%APACHE_PORT%", + "APACHE_IP_BINDING=%APACHE_IP_BINDING%", + "NEXTCLOUD_EXPORTER_CADDY_PASSWORD=%NEXTCLOUD_EXPORTER_CADDY_PASSWORD%" + ], + "volumes": [ + { + "source": "nextcloud_aio_caddy", + "destination": "/data", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/nextcloud", + "writeable": false + } + ], + "secrets": [ + "NEXTCLOUD_EXPORTER_CADDY_PASSWORD" + ], + "aio_variables": [ + "apache_ip_binding=@INTERNAL", + "apache_port=11000", + "turn_domain=%NC_DOMAIN%", + "talk_port=443" + ], + "nextcloud_exec_commands": [ + "mkdir '/mnt/ncdata/admin/files/nextcloud-aio-caddy'", + "touch '/mnt/ncdata/admin/files/nextcloud-aio-caddy/allowed-countries.txt'", + "echo 'Scanning nextcloud-aio-caddy folder for admin user...'", + "php /var/www/html/occ files:scan --path='/admin/files/nextcloud-aio-caddy'" + ] + } + ] +} diff --git a/community-containers/caddy/readme.md b/community-containers/caddy/readme.md new file mode 100644 index 00000000..fd2f30ef --- /dev/null +++ b/community-containers/caddy/readme.md @@ -0,0 +1,27 @@ +## Caddy with geoblocking +This container bundles caddy and auto-configures it for you. It also covers [vaultwarden](https://github.com/nextcloud/all-in-one/tree/main/community-containers/vaultwarden) by listening on `bw.$NC_DOMAIN`, if installed. It also covers [stalwart](https://github.com/nextcloud/all-in-one/tree/main/community-containers/stalwart) by listening on `mail.$NC_DOMAIN`, if installed. It also covers [jellyfin](https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyfin) by listening on `media.$NC_DOMAIN`, if installed. It also covers [lldap](https://github.com/nextcloud/all-in-one/tree/main/community-containers/lldap) by listening on `ldap.$NC_DOMAIN`, if installed. It also covers [nocodb](https://github.com/nextcloud/all-in-one/tree/main/community-containers/nocodb) by listening on `tables.$NC_DOMAIN`, if installed. It also covers [jellyseerr](https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyseerr) by listening on `requests.$NC_DOMAIN`, if installed. It also covers [nextcloud-exporter](https://github.com/nextcloud/all-in-one/tree/main/community-containers/nextcloud-exporter) by listening on `metrics.$NC_DOMAIN`, if installed. It also covers [LocalAI](https://github.com/nextcloud/all-in-one/tree/main/community-containers/local-ai) by listening on `ai.$NC_DOMAIN`, if installed. + +### Notes +- This container is incompatible with the [npmplus](https://github.com/nextcloud/all-in-one/tree/main/community-containers/npmplus) community container. So make sure that you do not enable both at the same time! +- Make sure that no other service is using port 443/tcp on your host as otherwise the containers will fail to start. You can check this with `sudo netstat -tulpn | grep 443` before installing AIO. +- Starting with AIO v12, the Talk port that was usually exposed on port 3478 is now set to port 443 udp and tcp and reachable via `your-nc-domain.com`. For the changes to become activated, you need to go to `https://your-nc-domain.com/settings/admin/talk` and delete all turn and stun servers. Then restart the containers and the new config should become active. +- Starting with AIO v12, you can also limit vaultwarden, stalwart and lldap to certain ip-addresses. You can do so by creating a `allowed-IPs-vaultwarden.txt`, `allowed-IPs-stalwart.txt`, or `allowed-IPs-lldap.txt` file in the `nextcloud-aio-caddy` directory of your admin user and adding the ip-addresses in these files. +- The container also supports the proxy protocol inside caddy. That means that you can run a supported web server in front of port 443/tcp and use the proxy protocol. You can enable this by configuring the `APACHE_IP_BINDING` environmental variable for the mastercontainer and set it to an ip-address from which the protocol shall be accepted. ⚠️ Note that the initial domain validation will not work correctly if you want to use the proxy protocol. So make sure to skip the domain validation in that case. See the [documentation](https://github.com/nextcloud/all-in-one#how-to-skip-the-domain-validation). +- If you want to use this with [vaultwarden](https://github.com/nextcloud/all-in-one/tree/main/community-containers/vaultwarden), make sure that you point `bw.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for vaultwarden. +- If you want to use this with [stalwart](https://github.com/nextcloud/all-in-one/tree/main/community-containers/stalwart), make sure that you point `mail.your-nc-domain.com` to your server using an A, AAAA or CNAME record so that caddy can get a certificate automatically for stalwart. +- If you want to use this with [jellyfin](https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyfin), make sure that you point `media.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for jellyfin. +- If you want to use this with [lldap](https://github.com/nextcloud/all-in-one/tree/main/community-containers/lldap), make sure that you point `ldap.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for lldap. +- If you want to use this with [nocodb](https://github.com/nextcloud/all-in-one/tree/main/community-containers/nocodb), make sure that you point `tables.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for nocodb. +- If you want to use this with [jellyseerr](https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyseerr), make sure that you point `requests.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for jellyseerr. +- If you want to use this with [nextcloud-exporter](https://github.com/nextcloud/all-in-one/tree/main/community-containers/nextcloud-exporter), make sure that you point `metrics.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for nextcloud-exporter. +- If you want to use this with [local AI](https://github.com/nextcloud/all-in-one/tree/main/community-containers/local-ai), make sure that you point `ai.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for local AI. +- After the container was started the first time, you should see a new `nextcloud-aio-caddy` folder and inside there an `allowed-countries.txt` file when you open the files app with the default `admin` user. In there you can adjust the allowed country codes for caddy by adding them to the first line, e.g. `IT FR` would allow access from italy and france. Private ip-ranges are always allowed. Additionally, in order to activate this config, you need to get an account at https://dev.maxmind.com/geoip/geolite2-free-geolocation-data and download the `GeoLite2-Country.mmdb` and upload it with this exact name into the `nextcloud-aio-caddy` folder. Afterwards restart all containers from the AIO interface and your new config should be active! +- You can add your own Caddy configurations in `/data/caddy-imports/` inside the Caddy container (`sudo docker exec -it nextcloud-aio-caddy bash`). These will be imported on container startup. **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack +- If you want to remove the container again and revert back to the default, you need to disable the container via the AIO-interface and follow https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#8-removing-the-reverse-proxy + +### Repository +https://github.com/szaimen/aio-caddy + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/calcardbackup/calcardbackup.json b/community-containers/calcardbackup/calcardbackup.json new file mode 100644 index 00000000..738ca2f0 --- /dev/null +++ b/community-containers/calcardbackup/calcardbackup.json @@ -0,0 +1,37 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-calcardbackup", + "display_name": "Calendar and contacts backup", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/calcardbackup", + "image": "waja/calcardbackup", + "image_tag": "latest", + "restart": "unless-stopped", + "environment": [ + "CRON_TIME=0 0 * * *", + "INIT_BACKUP=yes", + "BACKUP_DIR=/backup", + "NC_DIR=/nextcloud", + "NC_HOST=%NC_DOMAIN%", + "DB_HOST=nextcloud-aio-database", + "DB_PORT=5432", + "CALCARD_OPTS=-ltm 5" + ], + "volumes": [ + { + "source": "nextcloud_aio_calcardbackup", + "destination": "/backup", + "writeable": true + }, + { + "source": "nextcloud_aio_nextcloud", + "destination": "/nextcloud", + "writeable": false + } + ], + "backup_volumes": [ + "nextcloud_aio_calcardbackup" + ] + } + ] +} \ No newline at end of file diff --git a/community-containers/calcardbackup/readme.md b/community-containers/calcardbackup/readme.md new file mode 100644 index 00000000..42a218b4 --- /dev/null +++ b/community-containers/calcardbackup/readme.md @@ -0,0 +1,15 @@ +## calcardbackup +This container packages calcardbackup which is a tool that exports calendars and addressbooks from Nextcloud to .ics and .vcf files and saves them to a compressed file. + +### Notes +- Backups will be created at 00:00 UTC every day. Make sure that this does not conflict with the configured daily backups inside AIO. +- All the exports will be included in AIOs backup solution +- You can find the exports in the nextcloud_aio_calcardbackup volume +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/waja/docker-calcardbackup + +### Maintainer +https://github.com/pailloM + diff --git a/community-containers/container-management/container-management.json b/community-containers/container-management/container-management.json new file mode 100644 index 00000000..9563139d --- /dev/null +++ b/community-containers/container-management/container-management.json @@ -0,0 +1,41 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-container-management", + "display_name": "Container Management", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management", + "image": "ghcr.io/szaimen/aio-container-management", + "image_tag": "v1", + "internal_port": "5804", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "5804", + "protocol": "tcp" + } + ], + "volumes": [ + { + "source": "%WATCHTOWER_DOCKER_SOCKET_PATH%", + "destination": "/var/run/docker.sock", + "writeable": false + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "SECURE_CONNECTION=1", + "WEB_AUTHENTICATION=1", + "USER_ID=0", + "GROUP_ID=0", + "WEB_AUTHENTICATION_USERNAME=container-management", + "WEB_AUTHENTICATION_PASSWORD=%CONTAINER_MANAGEMENT_PASSWORD%", + "WEB_LISTENING_PORT=5804" + ], + "secrets": [ + "CONTAINER_MANAGEMENT_PASSWORD" + ], + "ui_secret": "CONTAINER_MANAGEMENT_PASSWORD" + } + ] +} diff --git a/community-containers/container-management/readme.md b/community-containers/container-management/readme.md new file mode 100644 index 00000000..e8c17313 --- /dev/null +++ b/community-containers/container-management/readme.md @@ -0,0 +1,15 @@ +## Container-Management +This container allows to manage insides of other containers via a GUI inside a Web session by allowing to run docker commands from inside this container. + +### Notes +- After adding and starting the container, you need to visit `https://ip.address.of.this.server:5804` in order to log in with the user `container-management` and the password that you can see next to the container in the AIO interface. (The web page uses a self-signed certificate, so you need to accept the warning). +- Then, you should see a terminal. There you can use any docker command. ⚠️ Be very carefully while doing that as can break your instance! +- There are also some pre-made scripts that make configuring some of the community containers easier. For example scripts for [LLDAP](https://github.com/nextcloud/all-in-one/tree/main/community-containers/lldap) and [Facerecognition](https://github.com/nextcloud/all-in-one/tree/main/community-containers/facerecognition). +- ⚠️ After you are done doing your operations, remove the container for better security again from the stack: https://github.com/nextcloud/all-in-one/tree/main/community-containers#how-to-remove-containers-from-aios-stack +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-container-management + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/dlna/dlna.json b/community-containers/dlna/dlna.json new file mode 100644 index 00000000..ac37e4ec --- /dev/null +++ b/community-containers/dlna/dlna.json @@ -0,0 +1,39 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-dlna", + "display_name": "DLNA", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/dlna", + "image": "thanek/nextcloud-dlna", + "image_tag": "latest", + "internal_port": "host", + "restart": "unless-stopped", + "depends_on": [ + "nextcloud-aio-database" + ], + "environment": [ + "NC_DOMAIN=%NC_DOMAIN%", + "NC_PORT=443", + "NEXTCLOUD_DLNA_SERVER_PORT=9999", + "NEXTCLOUD_DLNA_FRIENDLY_NAME=nextcloud-aio", + "NEXTCLOUD_DATA_DIR=/data", + "NEXTCLOUD_DB_TYPE=postgres", + "NEXTCLOUD_DB_HOST=%AIO_DATABASE_HOST%", + "NEXTCLOUD_DB_PORT=5432", + "NEXTCLOUD_DB_NAME=nextcloud_database", + "NEXTCLOUD_DB_USER=oc_nextcloud", + "NEXTCLOUD_DB_PASS=%DATABASE_PASSWORD%" + ], + "secrets": [ + "DATABASE_PASSWORD" + ], + "volumes": [ + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/data", + "writeable": false + } + ] + } + ] +} diff --git a/community-containers/dlna/readme.md b/community-containers/dlna/readme.md new file mode 100644 index 00000000..47502dc8 --- /dev/null +++ b/community-containers/dlna/readme.md @@ -0,0 +1,14 @@ +## DLNA server +This container bundles DLNA server for your Nextcloud files to be accessible by the clients in your local network. Simply run the container and look for a new media server `nextcloud-aio` in your local network. + +### Notes +- This container will work only if the Nextcloud installation is in your home network, it is not suitable for installations on remote servers. +- If you have a firewall like ufw configured, you might need to open at least port 9999 TCP and 1900 UDP first in order to make it work. +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/thanek/nextcloud-dlna + +### Maintainer +https://github.com/thanek + diff --git a/community-containers/facerecognition/facerecognition.json b/community-containers/facerecognition/facerecognition.json new file mode 100644 index 00000000..d97d5c08 --- /dev/null +++ b/community-containers/facerecognition/facerecognition.json @@ -0,0 +1,40 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-facerecognition", + "display_name": "Computing container for facerecognition", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/facerecognition", + "image": "matiasdelellis/facerecognition-external-model", + "image_tag": "v1", + "internal_port": "5000", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%", + "API_KEY=%FACERECOGNITION_API_KEY%", + "FACE_MODEL=3" + ], + "aio_variables": [ + "nextcloud_memory_limit=2048M" + ], + "secrets": [ + "FACERECOGNITION_API_KEY" + ], + "enable_nvidia_gpu": false, + "nextcloud_exec_commands": [ + "php /var/www/html/occ app:install facerecognition", + "php /var/www/html/occ app:enable facerecognition", + "php /var/www/html/occ config:system:set facerecognition.external_model_url --value nextcloud-aio-facerecognition:5000", + "php /var/www/html/occ config:system:set facerecognition.external_model_api_key --value %FACERECOGNITION_API_KEY%", + "php /var/www/html/occ face:setup -m 5", + "php /var/www/html/occ face:setup -M 1G", + "php /var/www/html/occ config:app:set facerecognition analysis_image_area --value 4320000", + "php /var/www/html/occ config:system:set enabledFaceRecognitionMimetype 0 --value image/jpeg", + "php /var/www/html/occ config:system:set enabledFaceRecognitionMimetype 1 --value image/png", + "php /var/www/html/occ config:system:set enabledFaceRecognitionMimetype 2 --value image/heic", + "php /var/www/html/occ config:system:set enabledFaceRecognitionMimetype 3 --value image/tiff", + "php /var/www/html/occ config:system:set enabledFaceRecognitionMimetype 4 --value image/webp", + "php /var/www/html/occ face:background_job --defer-clustering &" + ] + } + ] +} diff --git a/community-containers/facerecognition/readme.md b/community-containers/facerecognition/readme.md new file mode 100644 index 00000000..474ed1e2 --- /dev/null +++ b/community-containers/facerecognition/readme.md @@ -0,0 +1,34 @@ +## Facerecognition +This container bundles the external model of facerecognition and auto-configures it for you. + +### Notes +- This container needs imaginary in order to analyze modern file format images. Make sure to enable imaginary in the AIO interface before adding this container. +- The image analysis is currently set to fixed value of `1G`. See [this](https://github.com/search?q=repo%3Anextcloud%2Fall-in-one+1G+path%3A%2F%5Ecommunity-containers%5C%2Ffacerecognition%5C%2F%2F&type=code) +- Facerecognition is by default disabled for all users. If you want to enable facerecognition for all users, you can run the following commands before adding this container:
+**Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management. This script below can be run from inside the container-management container via `bash /facerecognition.sh`. + ```bash + # Go into the container + sudo docker exec --user www-data -it nextcloud-aio-nextcloud bash + ``` + Now inside the container: + ```bash + NC_USERS_NEW=$(php occ user:list | sed 's|^ - ||g' | sed 's|:.*||') + mapfile -t NC_USERS_NEW <<< "$NC_USERS_NEW" + for user in "${NC_USERS_NEW[@]}" + do + php occ user:setting "$user" facerecognition full_image_scan_done false + php occ user:setting "$user" facerecognition enabled true + done + + # Exit the container shell + exit + ``` +- If facerecognition shall analyze shared files & folders (`sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ config:app:set facerecognition handle_shared_files --value true`), groupfolders (`sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ config:app:set facerecognition handle_group_files --value true`) and/or external storages (`sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ config:app:set facerecognition handle_external_files --value true`) in Nextcloud, you need to enable support for it manually first by running the mentioned commands before adding this container. See https://github.com/matiasdelellis/facerecognition/wiki/Settings#hidden-settings for further notes on each of these settings.
+**Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/matiasdelellis/facerecognition-external-model + +### Maintainer +https://github.com/matiasdelellis diff --git a/community-containers/fail2ban/fail2ban.json b/community-containers/fail2ban/fail2ban.json new file mode 100644 index 00000000..78bf0a85 --- /dev/null +++ b/community-containers/fail2ban/fail2ban.json @@ -0,0 +1,42 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-fail2ban", + "display_name": "Fail2ban", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/fail2ban", + "image": "ghcr.io/szaimen/aio-fail2ban", + "image_tag": "v1", + "internal_port": "host", + "restart": "unless-stopped", + "cap_add": [ + "NET_ADMIN", + "NET_RAW" + ], + "environment": [ + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "nextcloud_aio_nextcloud", + "destination": "/nextcloud", + "writeable": false + }, + { + "source": "nextcloud_aio_vaultwarden_logs", + "destination": "/vaultwarden", + "writeable": false + }, + { + "source": "nextcloud_aio_jellyfin", + "destination": "/jellyfin", + "writeable": false + }, + { + "source": "nextcloud_aio_jellyseerr", + "destination": "/jellyseerr", + "writeable": false + } + ] + } + ] +} diff --git a/community-containers/fail2ban/readme.md b/community-containers/fail2ban/readme.md new file mode 100644 index 00000000..28ab21e3 --- /dev/null +++ b/community-containers/fail2ban/readme.md @@ -0,0 +1,14 @@ +## Fail2ban +This container bundles fail2ban and auto-configures it for you in order to block ip-addresses automatically. It also covers https://github.com/nextcloud/all-in-one/tree/main/community-containers/vaultwarden, https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyfin, and https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyseerr, if installed. + +### Notes +- If you get an error like `"ip6tables v1.8.9 (legacy): can't initialize ip6tables table filter': Table does not exist (do you need to insmod?)"`, you need to enable ip6tables on your host via `sudo modprobe ip6table_filter`. +- If you get an error like `stderr: 'iptables: No chain/target/match by that name.'` and `stderr: 'ip6tables: No chain/target/match by that name.'`, you need to follow https://github.com/szaimen/aio-fail2ban/issues/9#issuecomment-2026898790 in order to resolve this. +- You can unban ip addresses like so for example: `docker exec -it nextcloud-aio-fail2ban fail2ban-client set nextcloud unbanip 203.113.167.162`. **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-fail2ban + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/helloworld/helloworld.json b/community-containers/helloworld/helloworld.json new file mode 100644 index 00000000..fed10008 --- /dev/null +++ b/community-containers/helloworld/helloworld.json @@ -0,0 +1,12 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-helloworld", + "display_name": "Hello world", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/helloworld", + "image": "ghcr.io/docjyj/aio-helloworld", + "image_tag": "%AIO_CHANNEL%", + "restart": "unless-stopped" + } + ] +} diff --git a/community-containers/helloworld/readme.md b/community-containers/helloworld/readme.md new file mode 100644 index 00000000..83c557ac --- /dev/null +++ b/community-containers/helloworld/readme.md @@ -0,0 +1,8 @@ +## Hello World +This container is a template for creating a community container. + +### Repository +https://github.com/docjyj/aio-helloworld + +### Maintainer +https://github.com/docjyj diff --git a/community-containers/jellyfin/jellyfin.json b/community-containers/jellyfin/jellyfin.json new file mode 100644 index 00000000..f0840913 --- /dev/null +++ b/community-containers/jellyfin/jellyfin.json @@ -0,0 +1,40 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-jellyfin", + "display_name": "Jellyfin", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyfin", + "image": "jellyfin/jellyfin", + "image_tag": "latest", + "internal_port": "host", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "nextcloud_aio_jellyfin", + "destination": "/config", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/media", + "writeable": false + }, + { + "source": "%NEXTCLOUD_MOUNT%", + "destination": "%NEXTCLOUD_MOUNT%", + "writeable": true + } + ], + "devices": [ + "/dev/dri" + ], + "enable_nvidia_gpu": true, + "backup_volumes": [ + "nextcloud_aio_jellyfin" + ] + } + ] +} diff --git a/community-containers/jellyfin/readme.md b/community-containers/jellyfin/readme.md new file mode 100644 index 00000000..2a78bc1e --- /dev/null +++ b/community-containers/jellyfin/readme.md @@ -0,0 +1,20 @@ +## Jellyfin +This container bundles Jellyfin and auto-configures it for you. + +### Notes +- This container is incompatible with the [Plex](https://github.com/nextcloud/all-in-one/tree/main/community-containers/plex) community container. So make sure that you do not enable both at the same time! +- After adding and starting the container, you can directly visit http://ip.address.of.server:8096/ and access your new Jellyfin instance! +- This container should usually only be run in home networks as it exposes unencrypted services like DLNA by default which can be disabld via the web interface though. +- In order to access your Jellyfin outside the local network, you have to set up your own reverse proxy. You can set up a reverse proxy following [these instructions](https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md) and [Jellyfin's networking documentation](https://jellyfin.org/docs/general/networking/#running-jellyfin-behind-a-reverse-proxy), OR use the [Caddy](https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy) community container that will automatically configure `media.$NC_DOMAIN` to redirect to your Jellyfin. +- ⚠️ After the initial start, Jellyfin shows a configuration page to set up the root password, etc. **Be careful to initialize your Jellyfin before adding the DNS record.** +- If you have a firewall like ufw configured, you might need to open all Jellyfin ports in there first in order to make it work. Especially port 8096 is important! +- If you want to secure the installation with fail2ban, you might want to check out https://github.com/nextcloud/all-in-one/tree/main/community-containers/fail2ban +- The data of Jellyfin will be automatically included in AIO's backup solution! +- See [here](https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers) how to add it to the AIO stack. + + +### Repository +https://github.com/jellyfin/jellyfin + +### Maintainer +https://github.com/airopi diff --git a/community-containers/jellyseerr/jellyseerr.json b/community-containers/jellyseerr/jellyseerr.json new file mode 100644 index 00000000..64472a8a --- /dev/null +++ b/community-containers/jellyseerr/jellyseerr.json @@ -0,0 +1,35 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-jellyseerr", + "display_name": "Jellyseerr", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyseerr", + "image": "fallenbagel/jellyseerr", + "image_tag": "latest", + "internal_port": "5055", + "restart": "unless-stopped", + "init": false, + "ports": [ + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "5055", + "protocol": "tcp" + } + ], + "environment": [ + "PORT=5055", + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "nextcloud_aio_jellyseerr", + "destination": "/app/config", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_jellyseerr" + ] + } + ] +} diff --git a/community-containers/jellyseerr/readme.md b/community-containers/jellyseerr/readme.md new file mode 100644 index 00000000..0d8e049d --- /dev/null +++ b/community-containers/jellyseerr/readme.md @@ -0,0 +1,16 @@ +## Jellyseerr +This container bundles Jellyseerr and auto-configures it for you. + +### Notes +- This container is only intended to be used inside home networks as it uses http for its management page by default. +- After adding and starting the container, you can directly visit `http://ip.address.of.server:5055` and access your new Jellyseerr instance, which can be used to manage Plex, Jellyfin, and Emby. +- In order to access your Jellyseerr outside the local network, you have to set up your own reverse proxy. You can set up a reverse proxy following [these instructions](https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md) and [Jellyseerr's reverse proxy documentation.](https://docs.jellyseerr.dev/extending-jellyseerr/reverse-proxy), OR use the Caddy community container that will automatically configure requests.$NC_DOMAIN to redirect to your Jellyseerr. Note that it is recommended to [enable CSRF protection in Jellyseerr](https://docs.jellyseerr.dev/using-jellyseerr/settings/general#enable-csrf-protection) for added security if you plan to use Jellyseerr outside the local network, but make sure to read up on it and understand the caveats first. +- If you want to secure the installation with fail2ban, you might want to check out https://github.com/nextcloud/all-in-one/tree/main/community-containers/fail2ban. Note that [enabling the proxy support option in Jellyseerr](https://docs.jellyseerr.dev/using-jellyseerr/settings/general#enable-proxy-support) is required for this to work properly. +- The config of Jellyseerr will be automatically included in AIO's backup solution! +- See [here](https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers) how to add it to the AIO stack. + +### Repository +https://github.com/Fallenbagel/jellyseerr + +### Maintainer +https://github.com/Anvil5465 diff --git a/community-containers/languagetool/languagetool.json b/community-containers/languagetool/languagetool.json new file mode 100644 index 00000000..03eb6b39 --- /dev/null +++ b/community-containers/languagetool/languagetool.json @@ -0,0 +1,16 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-languagetool", + "display_name": "LanguageTool for Collabora", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/languagetool", + "image": "erikvl87/languagetool", + "image_tag": "latest", + "internal_port": "8010", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%" + ] + } + ] +} diff --git a/community-containers/languagetool/readme.md b/community-containers/languagetool/readme.md new file mode 100644 index 00000000..4c2ca98c --- /dev/null +++ b/community-containers/languagetool/readme.md @@ -0,0 +1,13 @@ +## LanguageTool for Collabora +This container bundles a LanguageTool for Collabora which adds spell checking functionality to Collabora. + +### Notes +- Make sure to have collabora enabled via the AIO interface +- After adding this container via the AIO Interface, while all containers are still stopped, you need to scroll down to the `Additional Collabora options` section and enter `--o:languagetool.enabled=true --o:languagetool.base_url=http://nextcloud-aio-languagetool:8010/v2`. +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/Erikvl87/docker-languagetool + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/libretranslate/libretranslate.json b/community-containers/libretranslate/libretranslate.json new file mode 100644 index 00000000..dad8d007 --- /dev/null +++ b/community-containers/libretranslate/libretranslate.json @@ -0,0 +1,34 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-libretranslate", + "display_name": "LibreTranslate (deprecated)", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/libretranslate", + "image": "ghcr.io/szaimen/aio-libretranslate", + "image_tag": "v1", + "internal_port": "5000", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "nextcloud_aio_libretranslate_db", + "destination": "/app/db", + "writeable": true + }, + { + "source": "nextcloud_aio_libretranslate_models", + "destination": "/home/libretranslate/.local", + "writeable": true + } + ], + "nextcloud_exec_commands": [ + "php /var/www/html/occ app:install integration_libretranslate", + "php /var/www/html/occ app:enable integration_libretranslate", + "php /var/www/html/occ config:app:set integration_libretranslate host --value='http://nextcloud-aio-libretranslate'", + "php /var/www/html/occ config:app:set integration_libretranslate port --value='5000'" + ] + } + ] +} diff --git a/community-containers/libretranslate/readme.md b/community-containers/libretranslate/readme.md new file mode 100644 index 00000000..f9893f34 --- /dev/null +++ b/community-containers/libretranslate/readme.md @@ -0,0 +1,22 @@ +## LibreTranslate +This container bundles LibreTranslate and auto-configures it for you. + +> [!WARNING] +> The LibreTranslate container and app is deprecated! +> Please use the [translate2 app](https://apps.nextcloud.com/apps/translate2) instead. +> You can activate it by first enabling the Docker-Socket-Proxy in the AIO-interface and then heading over to `https://your-nc-domain.com/settings/apps/tools` and installing and enabling the `Local Machine Translation` app. + +### Notes +- After the initial startup is done, you might want to change the default language to translate from and to via: +```bash +# Adjust the values `en` and `de` in commands below accordingly +sudo docker exec --user www-data nextcloud-aio-nextcloud php occ config:app:set integration_libretranslate from_lang --value="en" +sudo docker exec --user www-data nextcloud-aio-nextcloud php occ config:app:set integration_libretranslate to_lang --value="de" +``` +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-libretranslate + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/lldap/lldap.json b/community-containers/lldap/lldap.json new file mode 100644 index 00000000..32f8e7ec --- /dev/null +++ b/community-containers/lldap/lldap.json @@ -0,0 +1,47 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-lldap", + "display_name": "Light LDAP implementation", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/lldap", + "image": "lldap/lldap", + "image_tag": "v0-alpine", + "internal_port": "17170", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "17170", + "protocol": "tcp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "UID=65534", + "GID=65534", + "LLDAP_JWT_SECRET=%LLDAP_JWT_SECRET%", + "LLDAP_LDAP_USER_PASS=%LLDAP_LDAP_USER_PASS%", + "LLDAP_LDAP_BASE_DN=%NC_BASE_DN%" + ], + "secrets": [ + "LLDAP_JWT_SECRET", + "LLDAP_LDAP_USER_PASS" + ], + "ui_secret": "LLDAP_LDAP_USER_PASS", + "volumes": [ + { + "source": "nextcloud_aio_lldap", + "destination": "/data", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_lldap" + ], + "nextcloud_exec_commands": [ + "php /var/www/html/occ app:install user_ldap", + "php /var/www/html/occ app:enable user_ldap" + ] + } + ] +} diff --git a/community-containers/lldap/readme.md b/community-containers/lldap/readme.md new file mode 100644 index 00000000..586aea9e --- /dev/null +++ b/community-containers/lldap/readme.md @@ -0,0 +1,91 @@ +## Light LDAP server +This container bundles LLDAP server and auto-configures your Nextcloud instance for you. + +### Notes +- In order to access your LLDAP web interface outside the local network, you have to set up your own reverse proxy. You can set up a reverse proxy following [these instructions](https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md) OR use the [Caddy](https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy) community container that will automatically configure `ldap.$NC_DOMAIN` to redirect to your Lldap. You need to point the reverse proxy at port 17170 of this server. +- After adding and starting the container, you can log in to the lldap web interface by using the username `admin` and the secret that you can see next to the container in the AIO interface. +- To configure Nextcloud, you can use the generic configuration proposed below. +- For advanced configurations, see how to configure a client with lldap https://github.com/lldap/lldap#client-configuration +- Also, see how Nextcloud's LDAP application works https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Generic Nextcloud LDAP config +Functionality with this configuration: +- User and group management. +- Login via username (or email) and password. +- Profile picture sync. +- Synchronization of administrator accounts (via the lldap_admin group). + +> For simplicity, this configuration is done via the command line (don't worry, it's very simple). + +First, you need to retrieve the LLDAP admin password that you can see next to the container in the AIO interface. There you can configure smtp first and then invite users via mail. + +Now go into the Nextcloud container:
+**Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management. This script below can be run from inside the container-management container via `bash /lldap.sh`. +```bash +sudo docker exec --user www-data -it nextcloud-aio-nextcloud bash +``` +Now inside the container: +```bash +# Get Base +BASE_DN="dc=${NC_DOMAIN//./,dc=}" + +# Create a new empty ldap config +CONF_NAME=$(php /var/www/html/occ ldap:create-empty-config -p) + +# Check that the base DN matches your domain and retrieve your configuration name +echo "Base DN: '$BASE_DN', Config name: '$CONF_NAME'" + +# Set the ldap password +php /var/www/html/occ ldap:set-config $CONF_NAME ldapAgentPassword "" + +# Set the ldap config: Host and connection +php /var/www/html/occ ldap:set-config $CONF_NAME ldapAdminGroup lldap_admin +php /var/www/html/occ ldap:set-config $CONF_NAME ldapAgentName "cn=admin,ou=people,$BASE_DN" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapBase "$BASE_DN" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapHost "ldap://nextcloud-aio-lldap" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapPort 3890 +php /var/www/html/occ ldap:set-config $CONF_NAME ldapTLS 0 +php /var/www/html/occ ldap:set-config $CONF_NAME turnOnPasswordChange 0 + +# Set the ldap config: Users +php /var/www/html/occ ldap:set-config $CONF_NAME ldapBaseUsers "ou=people,$BASE_DN" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapEmailAttribute mail +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGidNumber gidNumber +php /var/www/html/occ ldap:set-config $CONF_NAME ldapLoginFilter "(&(|(objectclass=person))(|(uid=%uid)(|(mailPrimaryAddress=%uid)(mail=%uid))))" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapLoginFilterEmail 1 +php /var/www/html/occ ldap:set-config $CONF_NAME ldapLoginFilterUsername 1 +php /var/www/html/occ ldap:set-config $CONF_NAME ldapUserAvatarRule default +php /var/www/html/occ ldap:set-config $CONF_NAME ldapUserDisplayName cn +php /var/www/html/occ ldap:set-config $CONF_NAME ldapUserFilter "(|(objectclass=person))" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapUserFilterMode 0 +php /var/www/html/occ ldap:set-config $CONF_NAME ldapUserFilterObjectclass person + +# Set the ldap config: Groups +php /var/www/html/occ ldap:set-config $CONF_NAME ldapBaseGroups "ou=groups,$BASE_DN" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGroupDisplayName cn +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGroupFilter "(&(|(objectclass=groupOfUniqueNames)))" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGroupFilterMode 0 +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGroupFilterObjectclass groupOfUniqueNames +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGroupMemberAssocAttr uniqueMember +php /var/www/html/occ ldap:set-config $CONF_NAME useMemberOfToDetectMembership 1 + +# Optional : Check the configuration +#php /var/www/html/occ ldap:show-config $CONF_NAME + +# Test the ldap config +php /var/www/html/occ ldap:test-config $CONF_NAME + +# Enable ldap config +php /var/www/html/occ ldap:set-config $CONF_NAME ldapConfigurationActive 1 + +# Exit the container shell +exit +``` +It's done ! All you have to do is go to the Nextcloud administration interface to see the magic of LDAP. + +### Repository +https://github.com/lldap/lldap + +### Maintainer +https://github.com/docjyj diff --git a/community-containers/local-ai/local-ai.json b/community-containers/local-ai/local-ai.json new file mode 100644 index 00000000..e906b5a7 --- /dev/null +++ b/community-containers/local-ai/local-ai.json @@ -0,0 +1,63 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-local-ai", + "display_name": "Local AI", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/local-ai", + "image": "ghcr.io/docjyj/aio-local-ai-vulkan", + "image_tag": "v1", + "internal_port": "10078", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%", + "LOCALAI_API_KEY=%LOCALAI_API_KEY%", + "LOCALAI_ADDRESS=:10078", + "LOCALAI_CONFIG_DIR=/configuration", + "LOCALAI_MODEL_PATH=/models", + "LOCALAI_BACKEND_PATH=/backends" + ], + "ports": [ + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "10078", + "protocol": "tcp" + } + ], + "volumes": [ + { + "source": "nextcloud_aio_localai_configuration", + "destination": "/configuration", + "writeable": true + }, + { + "source": "nextcloud_aio_localai_models", + "destination": "/models", + "writeable": true + }, + { + "source": "nextcloud_aio_localai_backends", + "destination": "/backends", + "writeable": true + } + ], + "secrets": [ + "LOCALAI_API_KEY" + ], + "ui_secret": "LOCALAI_API_KEY", + "devices": [ + "/dev/dri" + ], + "nextcloud_exec_commands": [ + "php /var/www/html/occ app:install integration_openai", + "php /var/www/html/occ app:enable integration_openai", + "php /var/www/html/occ config:app:set integration_openai url --value http://nextcloud-aio-local-ai:10078", + "php /var/www/html/occ config:app:set integration_openai api_key --value %LOCALAI_API_KEY%", + "php /var/www/html/occ app:install assistant", + "php /var/www/html/occ app:enable assistant" + ], + "backup_volumes": [ + "nextcloud_aio_localai_configuration" + ] + } + ] +} diff --git a/community-containers/local-ai/readme.md b/community-containers/local-ai/readme.md new file mode 100644 index 00000000..02722bd0 --- /dev/null +++ b/community-containers/local-ai/readme.md @@ -0,0 +1,16 @@ +## Local AI +This container bundles Local AI and auto-configures it for you. It support hardware acceleration with Vulkan. + +### Notes +Documentation is available on the container repository. This documentation is regularly updated and is intended to be as simple and detailed as possible. Thanks for all your feedback! + +- See https://github.com/docjyJ/aio-local-ai-vulkan#getting-started for getting start with this container. +- See [this guide](https://github.com/nextcloud/all-in-one/discussions/5430) for how to improve AI task pickup speed +- Note that Nextcloud supports only one server for AI queries, so this container cannot be used at the same time as other AI containers. +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/docjyJ/aio-local-ai-vulkan + +### Maintainer +https://github.com/docjyJ diff --git a/community-containers/makemkv/makemkv.json b/community-containers/makemkv/makemkv.json new file mode 100644 index 00000000..22132cb8 --- /dev/null +++ b/community-containers/makemkv/makemkv.json @@ -0,0 +1,59 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-makekv", + "display_name": "MakeMKV", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/makemkv", + "image": "jlesage/makemkv", + "image_tag": "latest", + "internal_port": "5802", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "5802", + "protocol": "tcp" + } + ], + "volumes": [ + { + "source": "nextcloud_aio_makemkv", + "destination": "/config", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/storage", + "writeable": false + }, + { + "source": "%NEXTCLOUD_MOUNT%", + "destination": "/output", + "writeable": true + }, + { + "source": "/dev", + "destination": "/dev", + "writeable": false + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "SECURE_CONNECTION=1", + "WEB_AUTHENTICATION=1", + "USER_ID=33", + "GROUP_ID=33", + "WEB_AUTHENTICATION_USERNAME=makemkv", + "WEB_AUTHENTICATION_PASSWORD=%MAKEMKV_PASSWORD%", + "WEB_LISTENING_PORT=5802" + ], + "secrets": [ + "MAKEMKV_PASSWORD" + ], + "ui_secret": "MAKEMKV_PASSWORD", + "backup_volumes": [ + "nextcloud_aio_makemkv" + ] + } + ] +} diff --git a/community-containers/makemkv/readme.md b/community-containers/makemkv/readme.md new file mode 100644 index 00000000..e78510ee --- /dev/null +++ b/community-containers/makemkv/readme.md @@ -0,0 +1,20 @@ +## MakeMKV +This container bundles MakeMKV and auto-configures it for you. + +### Notes +- This container should only be run in home networks +- ⚠️ This container mounts all devices from the host inside the container in order to be able to access the external DVD/Blu-ray drives which is a security issue. However no better solution was found for the time being. +- This container only works on Linux and not on Docker-Desktop. +- This container requires the [`NEXTCLOUD_MOUNT` variable in AIO to be set](https://github.com/nextcloud/all-in-one#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host). Otherwise the output will not be saved correctly.. +- After adding and starting the container, you need to visit `https://internal.ip.of.server:5802` in order to log in with the `makemkv` user and the password that you can see next to the container in the AIO interface. (The web page uses a self-signed certificate, so you need to accept the warning). +- After the first login, you can adjust the `/output` directory in the MakeMKV settings to a subdirectory of the root of your chosen `NEXTCLOUD_MOUNT`. (by default `NEXTCLOUD_MOUNT` is mounted to `/output` inside the container. Thus all data is written to the root of it) +- The configured `NEXTCLOUD_DATADIR` is getting mounted to `/storage` inside the container. +- The config data of MakeMKV will be automatically included in AIOs backup solution! +- ⚠️ After you are done doing your operations, remove the container for better security again from the stack: https://github.com/nextcloud/all-in-one/tree/main/community-containers#how-to-remove-containers-from-aios-stack +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/jlesage/docker-makemkv + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/memories/memories.json b/community-containers/memories/memories.json new file mode 100644 index 00000000..cb84a722 --- /dev/null +++ b/community-containers/memories/memories.json @@ -0,0 +1,39 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-memories", + "display_name": "Memories Transcoder", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/memories", + "image": "radialapps/go-vod", + "image_tag": "latest", + "internal_port": "47788", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%", + "NEXTCLOUD_HOST=https://%NC_DOMAIN%" + ], + "volumes": [ + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/mnt/ncdata", + "writeable": false + }, + { + "source": "%NEXTCLOUD_MOUNT%", + "destination": "%NEXTCLOUD_MOUNT%", + "writeable": false + } + ], + "devices": [ + "/dev/dri" + ], + "enable_nvidia_gpu": true, + "nextcloud_exec_commands": [ + "php /var/www/html/occ app:install memories", + "php /var/www/html/occ app:enable memories", + "php /var/www/html/occ config:system:set memories.vod.external --value true --type bool", + "php /var/www/html/occ config:system:set memories.vod.connect --value nextcloud-aio-memories:47788" + ] + } + ] +} diff --git a/community-containers/memories/readme.md b/community-containers/memories/readme.md new file mode 100644 index 00000000..88a44c4d --- /dev/null +++ b/community-containers/memories/readme.md @@ -0,0 +1,12 @@ +## Memories +This container bundles the hardware-transcoding container of memories and auto-configures it for you. + +### Notes +- In order to actually enable the hardware transcoding, you need to add the following flag to AIO apart from adding this container: https://github.com/nextcloud/all-in-one#how-to-enable-hardware-acceleration-for-nextcloud +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/pulsejet/memories + +### Maintainer +https://github.com/pulsejet diff --git a/community-containers/minio/minio.json b/community-containers/minio/minio.json new file mode 100644 index 00000000..50613202 --- /dev/null +++ b/community-containers/minio/minio.json @@ -0,0 +1,41 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-minio", + "image_tag": "v2", + "display_name": "Minio S3 Storage", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/minio", + "image": "ghcr.io/szaimen/aio-minio", + "internal_port": "9000", + "environment": [ + "MINIO_ROOT_USER=nextcloud", + "MINIO_ROOT_PASSWORD=%MINIO_ROOT_PASSWORD%" + ], + "secrets": [ + "MINIO_ROOT_PASSWORD" + ], + "volumes": [ + { + "source": "nextcloud_aio_minio", + "destination": "/data", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_minio" + ], + "nextcloud_exec_commands": [ + "php /var/www/html/occ config:system:set objectstore class --value 'OC\\Files\\ObjectStore\\S3'", + "php /var/www/html/occ config:system:set objectstore arguments autocreate --value true --type bool", + "php /var/www/html/occ config:system:set objectstore arguments use_path_style --value true --type bool", + "php /var/www/html/occ config:system:set objectstore arguments use_ssl --value false --type bool", + "php /var/www/html/occ config:system:set objectstore arguments region --value ''", + "php /var/www/html/occ config:system:set objectstore arguments bucket --value nextcloud", + "php /var/www/html/occ config:system:set objectstore arguments key --value nextcloud", + "php /var/www/html/occ config:system:set objectstore arguments secret --value %MINIO_ROOT_PASSWORD%", + "php /var/www/html/occ config:system:set objectstore arguments port --value 9000", + "php /var/www/html/occ config:system:set objectstore arguments hostname --value nextcloud-aio-minio" + ] + } + ] +} diff --git a/community-containers/minio/readme.md b/community-containers/minio/readme.md new file mode 100644 index 00000000..be41d5bd --- /dev/null +++ b/community-containers/minio/readme.md @@ -0,0 +1,18 @@ +## Minio +This container bundles minio s3 storage and auto-configures it for you. + +>[!WARNING] +> Enabling this container will remove access to all the files formerly written to the data directory. +> So only enable this on a clean instance directly after installing AIO. +> All additional users that are added via Nextcloud afterwards are going to work correctly. +> Also, after enabling and using it, make sure to not disable the container as you cannot migrate from s3 to local storage anymore and s3 is a critical part of your infrastructure from then on. + +### Notes +- The data of Minio will be automatically included in AIOs backup solution! +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-minio + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/nextcloud-exporter/nextcloud-exporter.json b/community-containers/nextcloud-exporter/nextcloud-exporter.json new file mode 100644 index 00000000..e5bf74b8 --- /dev/null +++ b/community-containers/nextcloud-exporter/nextcloud-exporter.json @@ -0,0 +1,35 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-nextcloud-exporter", + "display_name": "Prometheus Nextcloud Exporter", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/nextcloud-exporter", + "image": "ghcr.io/xperimental/nextcloud-exporter", + "image_tag": "0.9.0", + "internal_port": "9205", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "127.0.0.1", + "port_number": "9205", + "protocol": "tcp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "NEXTCLOUD_SERVER=https://%NC_DOMAIN%", + "NEXTCLOUD_AUTH_TOKEN=%NEXTCLOUD_EXPORTER_TOKEN%", + "NEXTCLOUD_LISTEN_ADDRESS=0.0.0.0:9205", + "NEXTCLOUD_TIMEOUT=5s" + ], + "ui_secret": "NEXTCLOUD_EXPORTER_CADDY_PASSWORD", + "secrets": [ + "NEXTCLOUD_EXPORTER_TOKEN", + "NEXTCLOUD_EXPORTER_CADDY_PASSWORD" + ], + "nextcloud_exec_commands": [ + "php /var/www/html/occ config:app:set serverinfo token --value %NEXTCLOUD_EXPORTER_TOKEN%" + ] + } + ] +} diff --git a/community-containers/nextcloud-exporter/readme.md b/community-containers/nextcloud-exporter/readme.md new file mode 100644 index 00000000..3efa6257 --- /dev/null +++ b/community-containers/nextcloud-exporter/readme.md @@ -0,0 +1,72 @@ +## Prometheus Nextcloud Exporter + +A Prometheus exporter that collects metrics from your Nextcloud instance for monitoring and alerting. + +### How to install + +See the [Community Containers documentation](https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers) for instructions on how to install this in your Nextcloud All-in-One setup. + +### Security & Access + +**Important:** This container is configured to bind only to `127.0.0.1` (localhost) for security reasons. Prometheus exporters typically don't include authentication, so direct network exposure is not recommended. + +#### Access Options + +1. **With Caddy Container (Recommended)**: If you also install the [Caddy community container](https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy), it will automatically configure secure HTTPS access to your metrics with authentication at `metrics.your-domain.com` + + **Getting Authentication Credentials**: + - **Username**: Always `metrics` + - **Password**: After deploying the nextcloud-exporter container, the automatically generated password will be displayed in the AIO interface. Look for it in the container section below the container name "Prometheus Nextcloud Exporter". + +2. **Custom Reverse Proxy**: Set up your own reverse proxy (nginx, Apache, etc.) to provide HTTPS and authentication. See configuration guides: + - [NGINX Authentication](https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html) + [Reverse Proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) + - [Apache Authentication](https://httpd.apache.org/docs/2.4/howto/auth.html) + [Reverse Proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html) + - [Traefik BasicAuth](https://doc.traefik.io/traefik/middlewares/http/basicauth/) + - [Prometheus Security Best Practices](https://prometheus.io/docs/operating/security/) + +3. **Direct Local Access**: Access metrics directly from the server at `http://127.0.0.1:9205/metrics` (no authentication) + +### What it monitors +- User activity (active users hourly, daily) +- File counts and storage usage +- System health and database size +- App statistics and update availability +- Nextcloud performance metrics + +### Prometheus Configuration + +For **local server access** (if Prometheus runs on the same server): +```yaml +scrape_configs: + - job_name: 'nextcloud' + scrape_interval: 90s + static_configs: + - targets: ['127.0.0.1:9205'] + metrics_path: /metrics + scheme: http +``` + +For **Caddy integration** (secure external access): +```yaml +scrape_configs: + - job_name: 'nextcloud' + scrape_interval: 90s + static_configs: + - targets: ['metrics.your-domain.com'] + metrics_path: / + scheme: https + basic_auth: + username: 'metrics' + password: 'your-generated-password' +``` + +### Visualization + +Compatible with Grafana for creating monitoring dashboards: +- Pre-built dashboard available: [Grafana Dashboard #20716](https://grafana.com/grafana/dashboards/20716-nextcloud/) + +### Repository +https://github.com/xperimental/nextcloud-exporter + +### Maintainer +https://github.com/grotax diff --git a/community-containers/nocodb/nocodb.json b/community-containers/nocodb/nocodb.json new file mode 100644 index 00000000..e93d173c --- /dev/null +++ b/community-containers/nocodb/nocodb.json @@ -0,0 +1,44 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-nocodb", + "display_name": "NocoDB (deprecated)", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/nocodb", + "image": "nocodb/nocodb", + "image_tag": "latest", + "internal_port": "10028", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "10028", + "protocol": "tcp" + } + ], + "environment": [ + "NC_AUTH_JWT_SECRET=%NOCODB_JWT_SECRET%", + "NC_PUBLIC_URL=https://tables.%NC_DOMAIN%/", + "NC_DASHBOARD_URL=/", + "NC_ADMIN_EMAIL=admin@noco.db", + "NC_ADMIN_PASS=%NOCODB_USER_PASS%", + "PORT=10028", + "NC_DISABLE_TELE=true" + ], + "secrets": [ + "NOCODB_JWT_SECRET", + "NOCODB_USER_PASS" + ], + "ui_secret": "NOCODB_USER_PASS", + "volumes": [ + { + "source": "nextcloud_aio_nocodb", + "destination": "/usr/app/data", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_nocodb" + ] + } + ] +} diff --git a/community-containers/nocodb/readme.md b/community-containers/nocodb/readme.md new file mode 100644 index 00000000..fa23f8f6 --- /dev/null +++ b/community-containers/nocodb/readme.md @@ -0,0 +1,33 @@ +> [!CAUTION] +> NocoDB is licensed under a non-free license. +> +> And is no longer maintained. + +> [!NOTE] +> This container is there to compensate for the lack of functionality in Nextcloud Tables. +> +> When Nextcloud Tables V2 is released, I will stop checking for updates, and will no longer fix any potential issues. +> +> Some missing functionality in Nextcloud Tables: +> - Multiple view layout (Gantt, Kanban, Calendar...) +> - Field (Person, Tag, File...) +> - See more here https://github.com/nextcloud/tables/issues/103 + +## NocoDb server +This container bundles NocoDb without synchronization with Nextcloud. + +This is an alternative of **Airtable**. + +### Notes +- You need to configure a reverse proxy in order to run this container since nocodb needs a dedicated (sub)domain! For that, you might have a look at https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy. +- Currently, only `tables.$NC_DOMAIN` is supported as subdomain! So if Nextcloud is using `your-domain.com`, nocodb will use `tables.your-domain.com`. +- The data of NocoDb will be automatically included in AIOs backup solution! +- After adding and starting the container, you can log in to the web interface at `https://tables.$NC_DOMAIN/#/signin` with the username `admin@noco.db` and the password that you can see in the AIO interface next to the container. +- See https://docs.nocodb.com/ for usage of NocoDb +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/nocodb/nocodb + +### Maintainer +https://github.com/docjyJ diff --git a/community-containers/notifications/notifications.json b/community-containers/notifications/notifications.json new file mode 100644 index 00000000..5d886ec9 --- /dev/null +++ b/community-containers/notifications/notifications.json @@ -0,0 +1,23 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-notifications", + "display_name": "Notifications", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/notifications", + "image": "ghcr.io/szaimen/aio-notifications", + "image_tag": "v1", + "internal_port": "10000", + "restart": "unless-stopped", + "volumes": [ + { + "source": "%WATCHTOWER_DOCKER_SOCKET_PATH%", + "destination": "/var/run/docker.sock", + "writeable": false + } + ], + "environment": [ + "TZ=%TIMEZONE%" + ] + } + ] +} diff --git a/community-containers/notifications/readme.md b/community-containers/notifications/readme.md new file mode 100644 index 00000000..78ec49b6 --- /dev/null +++ b/community-containers/notifications/readme.md @@ -0,0 +1,12 @@ +## Notifications +This container allows other AIO community containers to send admin notifications to Nextcloud users. + +### Notes +- It needs to be enabled for the [scrutiny container](https://github.com/nextcloud/all-in-one/tree/main/community-containers/scrutiny) for example to make use of admin notifications that are sent if a smartctl failure was found. +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-notifications + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/npmplus/npmplus.json b/community-containers/npmplus/npmplus.json new file mode 100644 index 00000000..4b666c03 --- /dev/null +++ b/community-containers/npmplus/npmplus.json @@ -0,0 +1,32 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-npmplus", + "display_name": "NPMplus", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/npmplus", + "image": "ghcr.io/zoeyvid/npmplus", + "image_tag": "latest", + "internal_port": "host", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%", + "NC_AIO=true", + "NC_DOMAIN=%NC_DOMAIN%" + ], + "volumes": [ + { + "source": "nextcloud_aio_npmplus", + "destination": "/data", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_npmplus" + ], + "aio_variables": [ + "apache_ip_binding=127.0.0.1", + "apache_port=11000" + ] + } + ] +} diff --git a/community-containers/npmplus/readme.md b/community-containers/npmplus/readme.md new file mode 100644 index 00000000..a71b4af2 --- /dev/null +++ b/community-containers/npmplus/readme.md @@ -0,0 +1,20 @@ +## NPMplus +This container contains a fork of the Nginx Proxy Manager, which is a WebUI for nginx. It will also automatically create a config and cert for AIO. + +### Notes +- This container is incompatible with the [caddy](https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy) community container. So make sure that you do not enable both at the same time! +- Make sure that no other service is using port `443 (tcp/upd)` or `81 (tcp)` on your host as otherwise the containers will fail to start. You can check this with `sudo netstat -tulpn | grep "443\|81"` before installing AIO. +- Please change the default login data first, after you can read inside the logs that the default config for AIO is created and there are no errors. +- After the container was started the first time, please check the logs for errors. Then you can open NPMplus on `https://:81` and change the password. +- The default password is `iArhP1j7p1P6TA92FA2FMbbUGYqwcYzxC4AVEe12Wbi94FY9gNN62aKyF1shrvG4NycjjX9KfmDQiwkLZH1ZDR9xMjiG2QmoHXi` and the default email is `admin@example.org` +- If you want to use NPMplus behind a domain and outside localhost just create a new proxy host inside the NPMplus which proxies to `https`, `127.0.0.1` and port `81` - all other settings should be the same as for the AIO host. +- If you want to set env options from this [compose.yaml](https://github.com/ZoeyVid/NPMplus/blob/develop/compose.yaml), please set them inside the `.env` file which you can find in the `nextcloud_aio_npmplus` volume **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management +- The data (certs, configs, etc.) of NPMplus will be automatically included in AIOs backup solution! +- **Important:** you always need to enable https for your hosts, since `DISABLE_HTTP` is set to true by default +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository and Documentation +https://github.com/ZoeyVid/NPMplus + +### Maintainer +https://github.com/Zoey2936 diff --git a/community-containers/pi-hole/pi-hole.json b/community-containers/pi-hole/pi-hole.json new file mode 100644 index 00000000..2cecb9ec --- /dev/null +++ b/community-containers/pi-hole/pi-hole.json @@ -0,0 +1,57 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-pihole", + "display_name": "Pi-hole", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/pi-hole", + "image": "pihole/pihole", + "image_tag": "latest", + "internal_port": "8573", + "restart": "unless-stopped", + "init": false, + "ports": [ + { + "ip_binding": "", + "port_number": "53", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "53", + "protocol": "udp" + }, + { + "ip_binding": "", + "port_number": "8573", + "protocol": "tcp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "FTLCONF_webserver_api_password=%PIHOLE_WEBPASSWORD%", + "FTLCONF_dns_listeningMode=all", + "FTLCONF_webserver_port=8573" + ], + "volumes": [ + { + "source": "nextcloud_aio_pihole", + "destination": "/etc/pihole", + "writeable": true + }, + { + "source": "nextcloud_aio_pihole_dnsmasq", + "destination": "/etc/dnsmasq.d", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_pihole", + "nextcloud_aio_pihole_dnsmasq" + ], + "ui_secret": "PIHOLE_WEBPASSWORD", + "secrets": [ + "PIHOLE_WEBPASSWORD" + ] + } + ] +} diff --git a/community-containers/pi-hole/readme.md b/community-containers/pi-hole/readme.md new file mode 100644 index 00000000..7254f585 --- /dev/null +++ b/community-containers/pi-hole/readme.md @@ -0,0 +1,18 @@ +## Pi-hole +This container bundles pi-hole and auto-configures it for you. + +### Notes +- You should not run this container on a public VPS! It is only intended to run in home networks! +- Make sure that no dns server is already running by checking with `sudo netstat -tulpn | grep 53`. Otherwise the container will not be able to start! +- The DHCP functionality of Pi-hole has been disabled! +- The data of pi-hole will be automatically included in AIOs backup solution! +- After adding and starting the container, you can visit `http://ip.address.of.this.server:8573/admin` in order to log in with the admin key that you can see next to the container in the AIO interface. There you can configure the pi-hole setup. Also you can add local dns records. +- You can configure your home network now to use pi-hole as its dns server by configuring your router. +- Additionally, you can configure the docker daemon to use that by editing `/etc/docker/daemon.json` and adding ` { "dns" : [ "ip.address.of.this.server" , "8.8.8.8" ] } `. +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/pi-hole/docker-pi-hole + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/plex/plex.json b/community-containers/plex/plex.json new file mode 100644 index 00000000..5fc8712a --- /dev/null +++ b/community-containers/plex/plex.json @@ -0,0 +1,42 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-plex", + "display_name": "Plex", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/plex", + "image": "plexinc/pms-docker", + "image_tag": "latest", + "internal_port": "host", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%", + "PLEX_UID=33", + "PLEX_GID=33" + ], + "volumes": [ + { + "source": "nextcloud_aio_plex", + "destination": "/config", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/data", + "writeable": false + }, + { + "source": "%NEXTCLOUD_MOUNT%", + "destination": "%NEXTCLOUD_MOUNT%", + "writeable": false + } + ], + "devices": [ + "/dev/dri" + ], + "enable_nvidia_gpu": true, + "backup_volumes": [ + "nextcloud_aio_plex" + ] + } + ] +} diff --git a/community-containers/plex/readme.md b/community-containers/plex/readme.md new file mode 100644 index 00000000..7f8434ab --- /dev/null +++ b/community-containers/plex/readme.md @@ -0,0 +1,17 @@ +## Plex +This container bundles Plex and auto-configures it for you. + +### Notes +- This container is incompatible with the [Jellyfin](https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyfin) community container. So make sure that you do not enable both at the same time! +- This is not working on arm64 since Plex does only provide x64 docker images. +- This container should usually only be run in home networks as it exposes unencrypted services like DLNA by default which can be disabld via the web interface though. +- If you have a firewall like ufw configured, you might need to open all Plex ports in there first in order to make it work. Especially port 32400 is important! +- After adding and starting the container, you need to visit http://ip.address.of.server:32400/manage in order to claim your server with a plex account +- The data of Plex will be automatically included in AIOs backup solution! +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/plexinc/pms-docker + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/readme.md b/community-containers/readme.md new file mode 100644 index 00000000..1631a952 --- /dev/null +++ b/community-containers/readme.md @@ -0,0 +1,20 @@ +# Community containers +This directory features containers that are built for AIO which allows to add additional functionality very easily. + +## Disclaimers +All containers that are in this directory are community maintained so the responsibility is on the community to keep them updated and secure. There is no guarantee that this will be the case in the future. + +## How to use this? +Starting with v11 of AIO, the management of Community Containers is done via the AIO interface (it is the last section in the AIO interface, so only visible if you scroll down). +⚠️⚠️⚠️ Please review the folder for documentation on each of the containers before adding them! Not reviewing the documentation for each of them first might break starting the AIO containers because e.g. fail2ban only works on Linux and not on Docker Desktop! **Hint:** If the containers where running already, in order to actually start the added container, you need to click on `Stop containers` and the `Update and start containers` in order to actually start it. + +## How to add containers? +Simply submit a PR by creating a new folder in this directory: https://github.com/nextcloud/all-in-one/tree/main/community-containers with the name of your container. It must include a json file with the same name and with correct syntax and a readme.md with additional information. You might get inspired by caddy, fail2ban, local-ai, libretranslate, plex, pi-hole or vaultwarden (subfolders in this directory). For a full-blown example of the json file, see https://github.com/nextcloud/all-in-one/blob/main/php/containers.json. The json-schema that it validates against can be found here: https://github.com/nextcloud/all-in-one/blob/main/php/containers-schema.json. + +### Is there a list of ideas for new community containers? +Yes, see [this list](https://github.com/nextcloud/all-in-one/issues/5251) for already existing ideas for new community containers. Feel free to pick one up and add it to this folder by following the instructions above. + +## How to remove containers from AIOs stack? +You can remove containers now via the web interface. + +After removing the containers, there might be some data left on your server that you might want to remove. You can get rid of the data by first running `sudo docker rm nextcloud-aio-container1`, (adjust `container1` accordingly) per community-container that you removed. Then run `sudo docker image prune -a` in order to remove all images that are not used anymore. As last step you can get rid of persistent data of these containers that is stored in volumes. You can check if there is some by running `sudo docker volume ls` and look for any volume that matches the ones that you removed. If so, you can remove them with `sudo docker volume rm nextcloud_aio_volume-id` (of course you need to adjust the `volume-id`). **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management diff --git a/community-containers/scrutiny/readme.md b/community-containers/scrutiny/readme.md new file mode 100644 index 00000000..3bb728f7 --- /dev/null +++ b/community-containers/scrutiny/readme.md @@ -0,0 +1,16 @@ +## Scrutiny +This container bundles Scrutiny which is a frontend for SMART stats and auto-configures it for you. + +### Notes +- This container should only be run in home networks +- ⚠️ This container mounts all devices from the host inside the container in order to be able to access the drives and smartctl stats which is a security issue. However no better solution was found for the time being. +- This container only works on Linux and not on Docker-Desktop. +- After adding and starting the container, you need to visit `http://internal.ip.of.server:8000` which will show the dashboard for your drives. +- It supports sending notifications in case of a smartctl failure if you enable the notifications community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/notifications +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-scrutiny + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/scrutiny/scrutiny.json b/community-containers/scrutiny/scrutiny.json new file mode 100644 index 00000000..b367e497 --- /dev/null +++ b/community-containers/scrutiny/scrutiny.json @@ -0,0 +1,56 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-scrutiny", + "display_name": "Scrutiny", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/scrutiny", + "image": "ghcr.io/szaimen/aio-scrutiny", + "image_tag": "v2", + "internal_port": "8000", + "init": false, + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "8000", + "protocol": "tcp" + } + ], + "cap_add": [ + "SYS_RAWIO", + "SYS_ADMIN" + ], + "environment": [ + "TZ=%TIMEZONE%", + "SCRUTINY_WEB_LISTEN_PORT=8000", + "COLLECTOR_API_ENDPOINT=http://127.0.0.1:8000" + ], + "volumes": [ + { + "source": "nextcloud_aio_scrutiny", + "destination": "/opt/scrutiny/config", + "writeable": true + }, + { + "source": "nextcloud_aio_scrutiny_db", + "destination": "/opt/scrutiny/influxdb", + "writeable": true + }, + { + "source": "/run/udev", + "destination": "/run/udev", + "writeable": false + }, + { + "source": "/dev", + "destination": "/dev", + "writeable": false + } + ], + "backup_volumes": [ + "nextcloud_aio_scrutiny", + "nextcloud_aio_scrutiny_db" + ] + } + ] +} diff --git a/community-containers/smbserver/readme.md b/community-containers/smbserver/readme.md new file mode 100644 index 00000000..20d90c9f --- /dev/null +++ b/community-containers/smbserver/readme.md @@ -0,0 +1,14 @@ +## SMB-server +This container bundles an SMB-server and allows to configure it via a graphical shell script. + +### Notes +- This container should only be run in home networks +- After adding and starting the container, you need to visit `https://internal.ip.of.server:5803` in order to log in with the `smbserver` user and the password that you can see next to the container in the AIO interface. (The web page uses a self-signed certificate, so you need to accept the warning). Then type in `bash /smbserver.sh` and you will see a graphical UI for configuring the smb-server interactively. +- The config data of SMB-server will be automatically included in AIOs backup solution! +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-smbserver/ + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/smbserver/smbserver.json b/community-containers/smbserver/smbserver.json new file mode 100644 index 00000000..d095eb7a --- /dev/null +++ b/community-containers/smbserver/smbserver.json @@ -0,0 +1,60 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-smbserver", + "display_name": "SMB-server", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/smbserver", + "image": "ghcr.io/szaimen/aio-smbserver", + "image_tag": "v1", + "internal_port": "5803", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "5803", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "445", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "139", + "protocol": "tcp" + } + ], + "volumes": [ + { + "source": "nextcloud_aio_smbserver", + "destination": "/smbserver", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/mnt/ncdata", + "writeable": true + }, + { + "source": "%NEXTCLOUD_MOUNT%", + "destination": "/mnt", + "writeable": true + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "WEB_AUTHENTICATION_USERNAME=smbserver", + "WEB_AUTHENTICATION_PASSWORD=%SMBSERVER_PASSWORD%", + "WEB_LISTENING_PORT=5803" + ], + "secrets": [ + "SMBSERVER_PASSWORD" + ], + "ui_secret": "SMBSERVER_PASSWORD", + "backup_volumes": [ + "nextcloud_aio_smbserver" + ] + } + ] +} diff --git a/community-containers/stalwart/readme.md b/community-containers/stalwart/readme.md new file mode 100644 index 00000000..b34f04db --- /dev/null +++ b/community-containers/stalwart/readme.md @@ -0,0 +1,22 @@ +> [!CAUTION] +> Be aware that the mail server is the most difficult service to deploy. +> +> Do not use this feature as a main mail server or without a redundancy system and without knowledge. + +## Stalwart mail server +This container bundles stalwart mail server and auto-configures it for you. + +### Notes +Documentation is available on the container repository. +This documentation is regularly updated and is intended to be as simple and detailed as possible. +Thanks for all your feedback! + +- See https://github.com/docjyJ/aio-stalwart#getting-started for getting start with this container. +- See https://stalw.art/docs/faq for further faq and docs on the project +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/docjyj/aio-stalwart + +### Maintainer +https://github.com/docjyj diff --git a/community-containers/stalwart/stalwart.json b/community-containers/stalwart/stalwart.json new file mode 100644 index 00000000..b9a48091 --- /dev/null +++ b/community-containers/stalwart/stalwart.json @@ -0,0 +1,75 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-stalwart", + "display_name": "Stalwart", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/stalwart", + "image": "ghcr.io/docjyj/aio-stalwart", + "image_tag": "v3", + "internal_port": "10003", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "25", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "143", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "465", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "587", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "993", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "4190", + "protocol": "tcp" + }, + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "10003", + "protocol": "tcp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "NC_DOMAIN=%NC_DOMAIN%", + "STALWART_USER_PASS=%STALWART_USER_PASS%", + "CLAMAV_ENABLED=%CLAMAV_ENABLED%" + ], + "secrets": [ + "STALWART_USER_PASS" + ], + "ui_secret": "STALWART_USER_PASS", + "volumes": [ + { + "source": "nextcloud_aio_stalwart", + "destination": "/opt/stalwart-mail", + "writeable": true + }, + { + "source": "nextcloud_aio_caddy", + "destination": "/caddy", + "writeable": false + } + ], + "backup_volumes": [ + "nextcloud_aio_stalwart" + ] + } + ] +} diff --git a/community-containers/vaultwarden/readme.md b/community-containers/vaultwarden/readme.md new file mode 100644 index 00000000..81f37010 --- /dev/null +++ b/community-containers/vaultwarden/readme.md @@ -0,0 +1,17 @@ +## Vaultwarden +This container bundles vaultwarden and auto-configures it for you. + +### Notes +- You need to configure a reverse proxy in order to run this container since vaultwarden needs a dedicated (sub)domain! For that, you might have a look at https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy or follow https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md and https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples. You need to point the reverse proxy at port 8812 of this server. +- Currently, only `bw.$NC_DOMAIN` is supported as subdomain! So if Nextcloud is using `your-domain.com`, vaultwarden will use `bw.your-domain.com`. The reverse proxy and domain must be configured accordingly! +- If you want to secure the installation with fail2ban, you might want to check out https://github.com/nextcloud/all-in-one/tree/main/community-containers/fail2ban +- The data of Vaultwarden will be automatically included in AIOs backup solution! +- After adding and starting the container, you need to visit `https://bw.your-domain.com/admin` in order to log in with the admin key that you can see next to the container in the AIO interface. There you can configure smtp first and then invite users via mail. After this is done, you might disable the admin panel via the reverse proxy by blocking connections to the subdirectory. +- If using the caddy community container, the vaultwarden admin interface can be disabled by creating a `block-vaultwarden-admin` file in the `nextcloud-aio-caddy` folder when you open the Nextcloud files app with the default `admin` user. Afterwards restart all containers from the AIO interface and the admin interface should be disabled! You can unlock the admin interface by removing the file again and afterwards restarting the containers via the AIO interface. +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/dani-garcia/vaultwarden + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/vaultwarden/vaultwarden.json b/community-containers/vaultwarden/vaultwarden.json new file mode 100644 index 00000000..b94996fc --- /dev/null +++ b/community-containers/vaultwarden/vaultwarden.json @@ -0,0 +1,49 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-vaultwarden", + "display_name": "Vaultwarden", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/vaultwarden", + "image": "ghcr.io/dani-garcia/vaultwarden", + "image_tag": "alpine", + "internal_port": "8812", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "8812", + "protocol": "tcp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "ROCKET_PORT=8812", + "ADMIN_TOKEN=%VAULTWARDEN_ADMIN_TOKEN%", + "DOMAIN=https://bw.%NC_DOMAIN%", + "LOG_FILE=/logs/vaultwarden.log", + "LOG_LEVEL=warn", + "SIGNUPS_VERIFY=true", + "SIGNUPS_ALLOWED=false" + ], + "volumes": [ + { + "source": "nextcloud_aio_vaultwarden", + "destination": "/data", + "writeable": true + }, + { + "source": "nextcloud_aio_vaultwarden_logs", + "destination": "/logs", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_vaultwarden" + ], + "ui_secret": "VAULTWARDEN_ADMIN_TOKEN", + "secrets": [ + "VAULTWARDEN_ADMIN_TOKEN" + ] + } + ] +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 00000000..c18d92d3 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,81 @@ +name: nextcloud-aio # Add the container to the same compose project like all the sibling containers are added to automatically. +services: + nextcloud-aio-mastercontainer: + image: ghcr.io/nextcloud-releases/all-in-one:latest # This is the container image used. You can switch to ghcr.io/nextcloud-releases/all-in-one:beta if you want to help testing new releases. See https://github.com/nextcloud/all-in-one#how-to-switch-the-channel + init: true # This setting makes sure that signals from main process inside the container are correctly forwarded to children. See https://docs.docker.com/reference/compose-file/services/#init + restart: always # This makes sure that the container starts always together with the host OS. See https://docs.docker.com/reference/compose-file/services/#restart + container_name: nextcloud-aio-mastercontainer # This line is not allowed to be changed as otherwise AIO will not work correctly + volumes: + - nextcloud_aio_mastercontainer:/mnt/docker-aio-config # This line is not allowed to be changed as otherwise the built-in backup solution will not work + - /var/run/docker.sock:/var/run/docker.sock:ro # May be changed on macOS, Windows or docker rootless. See the applicable documentation. If adjusting, don't forget to also set 'WATCHTOWER_DOCKER_SOCKET_PATH'! + network_mode: bridge # This adds the container to the same network as docker run would do. Comment this line and uncomment the line below and the networks section at the end of the file if you want to define a custom MTU size for the docker network + # networks: ["nextcloud-aio"] + ports: + - 80:80 # Can be removed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md + - 8080:8080 # This is the AIO interface, served via https and self-signed certificate. See https://github.com/nextcloud/all-in-one#explanation-of-used-ports + - 8443:8443 # Can be removed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md + # security_opt: ["label:disable"] # Is needed when using SELinux. See https://github.com/nextcloud/all-in-one#are-there-known-problems-when-selinux-is-enabled + # environment: # Is needed when using any of the options below + # AIO_DISABLE_BACKUP_SECTION: false # Setting this to true allows to hide the backup section in the AIO interface. See https://github.com/nextcloud/all-in-one#how-to-disable-the-backup-section + # APACHE_PORT: 11000 # Is needed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md + # APACHE_IP_BINDING: 127.0.0.1 # Should be set when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) that is running on the same host. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md + # APACHE_ADDITIONAL_NETWORK: frontend_net # (Optional) Connect the apache container to an additional docker network. Needed when behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) running in a different docker network on same server. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md + # BORG_RETENTION_POLICY: --keep-within=7d --keep-weekly=4 --keep-monthly=6 # Allows to adjust borgs retention policy. See https://github.com/nextcloud/all-in-one#how-to-adjust-borgs-retention-policy + # COLLABORA_SECCOMP_DISABLED: false # Setting this to true allows to disable Collabora's Seccomp feature. See https://github.com/nextcloud/all-in-one#how-to-disable-collaboras-seccomp-feature + # DOCKER_API_VERSION: 1.44 # You can adjust the internally used docker api version with this variable. ⚠️⚠️⚠️ Warning: please note that only the default api version (unset this variable) is supported and tested by the maintainers of Nextcloud AIO. So use this on your own risk and things might break without warning. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-internally-used-docker-api-version + # FULLTEXTSEARCH_JAVA_OPTIONS: "-Xms1024M -Xmx1024M" # Allows to adjust the fulltextsearch java options. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-fulltextsearch-java-options + # NEXTCLOUD_DATADIR: /mnt/ncdata # Allows to set the host directory for Nextcloud's datadir. ⚠️⚠️⚠️ Warning: do not set or adjust this value after the initial Nextcloud installation is done! See https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir + # NEXTCLOUD_MOUNT: /mnt/ # Allows the Nextcloud container to access the chosen directory on the host. See https://github.com/nextcloud/all-in-one#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host + # NEXTCLOUD_UPLOAD_LIMIT: 16G # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-upload-limit-for-nextcloud + # NEXTCLOUD_MAX_TIME: 3600 # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-max-execution-time-for-nextcloud + # NEXTCLOUD_MEMORY_LIMIT: 512M # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-php-memory-limit-for-nextcloud + # NEXTCLOUD_TRUSTED_CACERTS_DIR: /path/to/my/cacerts # CA certificates in this directory will be trusted by the OS of the nextcloud container (Useful e.g. for LDAPS) See https://github.com/nextcloud/all-in-one#how-to-trust-user-defined-certification-authorities-ca + # NEXTCLOUD_STARTUP_APPS: deck twofactor_totp tasks calendar contacts notes # Allows to modify the Nextcloud apps that are installed on starting AIO the first time. See https://github.com/nextcloud/all-in-one#how-to-change-the-nextcloud-apps-that-are-installed-on-the-first-startup + # NEXTCLOUD_ADDITIONAL_APKS: imagemagick # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value. See https://github.com/nextcloud/all-in-one#how-to-add-os-packages-permanently-to-the-nextcloud-container + # NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS: imagick # This allows to add additional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value. See https://github.com/nextcloud/all-in-one#how-to-add-php-extensions-permanently-to-the-nextcloud-container + # NEXTCLOUD_ENABLE_DRI_DEVICE: true # This allows to enable the /dev/dri device for containers that profit from it. ⚠️⚠️⚠️ Warning: this only works if the '/dev/dri' device is present on the host! If it should not exist on your host, don't set this to true as otherwise the Nextcloud container will fail to start! See https://github.com/nextcloud/all-in-one#how-to-enable-hardware-acceleration-for-nextcloud + # NEXTCLOUD_ENABLE_NVIDIA_GPU: true # This allows to enable the NVIDIA runtime and GPU access for containers that profit from it. ⚠️⚠️⚠️ Warning: this only works if an NVIDIA gpu is installed on the server. See https://github.com/nextcloud/all-in-one#how-to-enable-hardware-acceleration-for-nextcloud. + # NEXTCLOUD_KEEP_DISABLED_APPS: false # Setting this to true will keep Nextcloud apps that are disabled in the AIO interface and not uninstall them if they should be installed. See https://github.com/nextcloud/all-in-one#how-to-keep-disabled-apps + # SKIP_DOMAIN_VALIDATION: false # This should only be set to true if things are correctly configured. See https://github.com/nextcloud/all-in-one#how-to-skip-the-domain-validation + # TALK_PORT: 3478 # This allows to adjust the port that the talk container is using which is exposed on the host. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-talk-port + # WATCHTOWER_DOCKER_SOCKET_PATH: /var/run/docker.sock # Needs to be specified if the docker socket on the host is not located in the default '/var/run/docker.sock'. Otherwise mastercontainer updates will fail. For macos it needs to be '/var/run/docker.sock' + +# # Optional: Caddy reverse proxy. See https://github.com/nextcloud/all-in-one/discussions/575 +# # Alternatively, use Tailscale if you don't have a domain yet. See https://github.com/nextcloud/all-in-one/discussions/6817 +# # Hint: You need to uncomment APACHE_PORT: 11000 above, adjust cloud.example.com to your domain and uncomment the necessary docker volumes at the bottom of this file in order to make it work +# # You can find further examples here: https://github.com/nextcloud/all-in-one/discussions/588 +# caddy: +# image: caddy:alpine +# restart: always +# container_name: caddy +# volumes: +# - caddy_certs:/certs +# - caddy_config:/config +# - caddy_data:/data +# - caddy_sites:/srv +# network_mode: "host" +# configs: +# - source: Caddyfile +# target: /etc/caddy/Caddyfile +# configs: +# Caddyfile: +# content: | +# # Adjust cloud.example.com to your domain below +# https://cloud.example.com:443 { +# reverse_proxy localhost:11000 +# } + +volumes: # If you want to store the data on a different drive, see https://github.com/nextcloud/all-in-one#how-to-store-the-filesinstallation-on-a-separate-drive + nextcloud_aio_mastercontainer: + name: nextcloud_aio_mastercontainer # This line is not allowed to be changed as otherwise the built-in backup solution will not work + # caddy_certs: + # caddy_config: + # caddy_data: + # caddy_sites: + +# # Adjust the MTU size of the docker network. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-mtu-size-of-the-docker-network +# networks: +# nextcloud-aio: +# name: nextcloud-aio +# driver_opts: +# com.docker.network.driver.mtu: 1440 diff --git a/develop.md b/develop.md index c286a615..457b3a76 100644 --- a/develop.md +++ b/develop.md @@ -2,6 +2,7 @@ If you want to switch to the develop channel, you simply stop and delete the mastercontainer and create a new one with a changed tag to develop: ```shell sudo docker run \ +--init \ --sig-proxy=false \ --name nextcloud-aio-mastercontainer \ --restart always \ @@ -10,7 +11,7 @@ sudo docker run \ --publish 8443:8443 \ --volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ --volume /var/run/docker.sock:/var/run/docker.sock:ro \ -nextcloud/all-in-one:develop +ghcr.io/nextcloud-releases/all-in-one:develop ``` And you are done :) It will now also select the developer channel for all other containers automatically. @@ -18,12 +19,19 @@ It will now also select the developer channel for all other containers automatic ## How to publish new releases? Simply use https://github.com/nextcloud/all-in-one/issues/180 as template. +## How to update existing instances to a new major Nextcloud version? +Simply use https://github.com/nextcloud/all-in-one/issues/6198 as template. + ## How to build new containers Go to https://github.com/nextcloud-releases/all-in-one/actions/workflows/repo-sync.yml and run the workflow that will first sync the repo and then build new container that automatically get published to `develop` and `develop-arm64`. ## How to test things correctly? +Before testing, make sure that at least the amd64 containers are built successfully by checking the last workflow here: https://github.com/nextcloud-releases/all-in-one/actions/workflows/build_images.yml. + There is a testing-VM available for the maintainer of AIO that allows for some final testing before releasing new version. See [this](https://cloud.nextcloud.com/apps/collectives/Nextcloud%20Handbook/Technical/AIO%20testing%20VM?fileId=6350152) for details. +Additionally, there are now E2E tests available that can be run via https://github.com/nextcloud/all-in-one/actions/workflows/playwright.yml + ## How to promote builds from develop to beta 1. Verify that no job is running here: https://github.com/nextcloud-releases/all-in-one/actions/workflows/build_images.yml 2. Go to https://github.com/nextcloud-releases/all-in-one/actions/workflows/promote-to-beta.yml, click on `Run workflow`. @@ -33,5 +41,32 @@ This is documented here: https://github.com/nextcloud-releases/all-in-one/tree/m ## How to promote builds from beta to latest +1. Verify that GitHub Services are running correctly: https://www.githubstatus.com/ 1. Verify that no job is running here: https://github.com/nextcloud-releases/all-in-one/actions/workflows/promote-to-beta.yml -2. Go to https://github.com/nextcloud-releases/all-in-one/actions/workflows/promote-to-latest.yml, click on `Run workflow`. +1. Go to https://github.com/nextcloud-releases/all-in-one/actions/workflows/promote-to-latest.yml, click on `Run workflow`. + +## How to connect to the database? +Simply run `sudo docker exec -it nextcloud-aio-database psql -U oc_nextcloud nextcloud_database` and you should be in. + +## How to locally build and test changes to mastercontainer +1. Ensure you are on the developer channel per the instructions above. +1. Use the commands below from the project root to build the mastercontainer image: +``` +docker buildx build --file Containers/mastercontainer/Dockerfile --tag ghcr.io/nextcloud-releases/all-in-one:develop --load . +``` +1. Start a container with above built image. +1. Since the hash of a locally built image doesn't match the latest release mastercontainer, it prompts for a mandatory update. To temporarily bypass the update suffix `?bypass_mastercontainer_update` to the URL. Eg: `https://localhost:8080/containers?bypass_mastercontainer_update` + +## How to locally build and test changes to other containers using the bypass_container_update param +1. Ensure you are on the developer channel per the instructions above. +1. Use the commands below from the project root to build the container image: +``` +# For the "nextcloud" container +docker buildx build --file Containers/nextcloud/Dockerfile --tag ghcr.io/nextcloud-releases/aio-nextcloud:develop --load . + +# For all other containers +docker buildx build --file Containers/{container}/Dockerfile --tag ghcr.io/nextcloud-releases/aio-{container}:develop --load Containers/{container} +``` +1. Stop the containers using the AIO interface. +1. Reload the AIO interface with the param `bypass_container_update` to avoid overwriting your local changes, e.g. `https://localhost:8080/containers?bypass_container_update`. +1. Click "Start and update containers" and test your changes. Containers will not be updated, despite the button text. diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 42688d0c..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -version: "3.8" - -volumes: - nextcloud_aio_mastercontainer: - name: nextcloud_aio_mastercontainer # This line is not allowed to be changed - -services: - nextcloud: - image: nextcloud/all-in-one:latest # Must be changed to 'nextcloud/all-in-one:latest-arm64' when used with an arm64 CPU - restart: always - container_name: nextcloud-aio-mastercontainer # This line is not allowed to be changed - volumes: - - nextcloud_aio_mastercontainer:/mnt/docker-aio-config # This line is not allowed to be changed - - /var/run/docker.sock:/var/run/docker.sock:ro # May be changed on macOS, Windows or docker rootless. See the applicable documentation - ports: - - 80:80 # Can be removed when running behind a reverse proxy. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md - - 8080:8080 - - 8443:8443 # Can be removed when running behind a reverse proxy. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md - # environment: # Is needed when using any of the options below - # - APACHE_PORT=11000 # Is needed when running behind a reverse proxy. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md - # - APACHE_IP_BINDING=127.0.0.1 # Should be set when running behind a reverse proxy that is running on the same host. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md - # - TALK_PORT=3478 # This allows to adjust the port that the talk container is using. - # - NEXTCLOUD_DATADIR=/mnt/ncdata # Allows to set the host directory for Nextcloud's datadir. See https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir - # - NEXTCLOUD_MOUNT=/mnt/ # Allows the Nextcloud container to access the chosen directory on the host. See https://github.com/nextcloud/all-in-one#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host - # - DOCKER_SOCKET_PATH=/var/run/docker.sock # Needs to be specified if the docker socket on the host is not located in the default '/var/run/docker.sock'. Otherwise mastercontainer updates will fail. - # - DISABLE_BACKUP_SECTION=true # Setting this to true allows to hide the backup section in the AIO interface. - # - NEXTCLOUD_UPLOAD_LIMIT=10G # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-upload-limit-for-nextcloud - # - NEXTCLOUD_MAX_TIME=3600 # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-max-execution-time-for-nextcloud - - # # Optional: Caddy reverse proxy. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md - # # You can find further examples here: https://github.com/nextcloud/all-in-one/discussions/588 - # caddy: - # image: caddy:alpine - # restart: always - # container_name: caddy - # volumes: - # - ./Caddyfile:/etc/caddy/Caddyfile - # - ./certs:/certs - # - ./config:/config - # - ./data:/data - # - ./sites:/srv - # network_mode: "host" diff --git a/docker-ipv6-support.md b/docker-ipv6-support.md new file mode 100644 index 00000000..a784e7c7 --- /dev/null +++ b/docker-ipv6-support.md @@ -0,0 +1,44 @@ +# IPv6-Support for Docker + +## Docker on Linux and Docker-rootless +First of all upgrade your docker installation to v27.0.1 or higher. +1. Then edit `/etc/docker/daemon.json` (or `~/.config/docker/daemon.json` in case of docker-rootless), add the below json: + +> [!WARNING] +> This will enable ipv6 for all new docker networks by default! You can alternatively create the `nextcloud-aio` network with ipv6 support by hand manually via docker network create or via compose.yaml. + +```json +{ + "default-network-opts": {"bridge":{"com.docker.network.enable_ipv6":"true"}} +} +``` + +And save the file. + +2. Reload the Docker configuration file. + +```console +sudo systemctl restart docker +``` + +3. Make sure that ipv6 is enabled for the internal `nextcloud-aio` network by running `sudo docker network inspect nextcloud-aio | grep EnableIPv6`. On a new instance, this command should return that it did not find a network with this name. Then you can run `sudo docker network create nextcloud-aio` in order to create the network with ipv6-support. However if it finds the network and its value `EnableIPv6` is set to false, make sure to follow https://github.com/nextcloud/all-in-one/discussions/4989 in order to recreate the network and enable ipv6 for it. + +## Docker Desktop (Windows and macOS) +First of all upgrade your docker desktop installation to v4.32.0 or higher. +Then, on Windows and macOS which use Docker Desktop, you need to go into the settings, and select `Docker Engine`. There you should see the currently used daemon.json file. + +1. You need to now adjust this json file: + +> [!WARNING] +> This will enable ipv6 for all new docker networks by default! You can alternatively create the `nextcloud-aio` network with ipv6 support by hand manually via docker network create or via compose.yaml. + +```json +"default-network-opts": {"bridge":{"com.docker.network.enable_ipv6":"true"}} +``` + +2. Add these values to the json and make sure to keep the other currently values and that you don't see `Unexpected token in JSON at position ...` before attempting to restart by clicking on `Apply & restart`. +3. Make sure that ipv6 is enabled for the internal `nextcloud-aio` network by running `sudo docker network inspect nextcloud-aio | grep EnableIPv6`. On a new instance, this command should return that it did not find a network with this name. Then you can run `sudo docker network create nextcloud-aio` in order to create the network with ipv6-support. However if it finds the network and its value `EnableIPv6` is set to false, make sure to follow https://github.com/nextcloud/all-in-one/discussions/4989 in order to recreate the network and enable ipv6 for it. + +--- + +**Note**: This is a copy of the original docker docs at https://docs.docker.com/config/daemon/ipv6/ which apparently are not correct. diff --git a/docker-rootless.md b/docker-rootless.md index 4bc6e6cd..f77b4a54 100644 --- a/docker-rootless.md +++ b/docker-rootless.md @@ -1,14 +1,39 @@ # Docker rootless +**Please note:** Due to a bug in Collabora is the Collabora container currently in rootless mode not working. See https://github.com/CollaboraOnline/online/issues/2800. In that case, you need to run a separate Collabora instance on your own if you want to use this feature. The following flag will be useful https://github.com/nextcloud/all-in-one#how-to-keep-disabled-apps. + You can run AIO with docker rootless by following the steps below. 0. If docker is already installed, you should consider disabling it first: (`sudo systemctl disable --now docker.service docker.socket`) 1. Install docker rootless by following the official documentation: https://docs.docker.com/engine/security/rootless/#install. The easiest way is installing it **Without packages** (`curl -fsSL https://get.docker.com/rootless | sh`). Further limitations, distribution specific hints, etc. are discussed on the same site. Also do not forget to enable the systemd service, which may not be enabled always by default. See https://docs.docker.com/engine/security/rootless/#usage. (`systemctl --user enable docker`) -1. If you need ipv6 support, you should enable it by following https://docs.docker.com/config/daemon/ipv6/. The daemon.json file is most likely stored in `~/.config/docker/daemon.json`. -1. Do not forget to set the mentioned environmental variables and in best case add them to your `~/.bashrc` file as shown! +1. If you need ipv6 support, you should enable it by following https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md. +1. Do not forget to set the mentioned environmental variables `PATH` and `DOCKER_HOST` and in best case add them to your `~/.bashrc` file as shown! 1. Also do not forget to run `loginctl enable-linger USERNAME` (and substitute USERNAME with the correct one) in order to make sure that user services are automatically started after every reboot. -1. Expose the privileged ports by following https://docs.docker.com/engine/security/rootless/#exposing-privileged-ports. (`sudo setcap cap_net_bind_service=ep $(which rootlesskit); systemctl --user restart docker`) -1. Use the official AIO startup command but use `--volume $XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock:ro` instead of `--volume /var/run/docker.sock:/var/run/docker.sock:ro` and also add `-e DOCKER_SOCKET_PATH=$XDG_RUNTIME_DIR/docker.sock` to the initial container startup (which is needed for mastercontainer updates to work correctly). -1. Now everything should work like without docker rootless. You can consider using docker-compose for this or running it behind a reverse proxy. Basically the only thing that needs to be adjusted always in the startup command or docker-compose file (after installing docker rootles) are things that are mentioned in point 3. +1. Expose the privileged ports by following https://docs.docker.com/engine/security/rootless/#exposing-privileged-ports. (`sudo setcap cap_net_bind_service=ep $(which rootlesskit); systemctl --user restart docker`). If you require the correct source IP you must expose them via `/etc/sysctl.conf`, [see note below](#note-regarding-docker-network-driver). +1. Use the official AIO startup command but use `--volume $XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock:ro` instead of `--volume /var/run/docker.sock:/var/run/docker.sock:ro` and also add `--env WATCHTOWER_DOCKER_SOCKET_PATH=$XDG_RUNTIME_DIR/docker.sock` to the initial container startup (which is needed for mastercontainer updates to work correctly). When you are using Portainer to deploy AIO, the variable `$XDG_RUNTIME_DIR` is not available. In this case, it is necessary to manually add the path (e.g. `/run/user/1000/docker.sock`) to the Docker compose file to replace the `$XDG_RUNTIME_DIR` variable. If you are not sure how to get the path, you can run on the host: `echo $XDG_RUNTIME_DIR`. +1. Now everything should work like without docker rootless. You can consider using docker-compose for this or running it behind a reverse proxy. Basically the only thing that needs to be adjusted always in the startup command or compose.yaml file (after installing docker rootles) are things that are mentioned in point 3. +1. ⚠️ **Important:** Please read through all notes below! -**Please note:** All files outside the containers get created, written to and accessed as the user that is running the docker daemon or a subuid of it. So for the built-in backup to work you need to allow this user to write to the target directory. For changing Nextcloud's datadir, you need to adjust the permissions of the chosen folders to be accessible/writeable by the userid `100032:100032` (if running `grep ^$(whoami): /etc/subuid` as the user that is running the docker daemon returns 100000 as first value). This logically also applies to the NEXTCLOUD_MOUNT option. +### Note regarding sudo in the documentation +Almost all commands in this project's documentation use `sudo docker ...`. Since `sudo` is not needed in case of docker rootless, you simply remove `sudo` from the commands and they should work. + +### Note regarding permissions +All files outside the containers get created, written to and accessed as the user that is running the docker daemon or a subuid of it. So for the built-in backup to work you need to allow this user to write to the target directory. E.g. with `sudo chown -R USERNAME:GROUPNAME /mnt/backup`. The same applies when changing Nextcloud's datadir via NEXTCLOUD_DATADIR. E.g. `sudo chown -R USERNAME:GROUPNAME /mnt/ncdata`. When you want to use the NEXTCLOUD_MOUNT option for local external storage, you need to adjust the permissions of the chosen folders to be accessible/writeable by the userid `100032:100032` (if running `grep ^$(whoami): /etc/subuid` as the user that is running the docker daemon returns 100000 as first value). + + +### Note regarding docker network driver +By default rootless docker uses the `slirp4netns` IP driver and the `builtin` port driver. As mentioned in [the documentation](https://docs.docker.com/engine/security/rootless/#networking-errors), this combination doesn't provide "Source IP propagation". This means that Apache and Nextcloud will see all connections as coming from the docker gateway (e.g 172.19.0.1), which can lead to the Nextcloud brute force protection blocking all connection attempts. To expose the correct source IP, you will need to configure docker to also use `slirp4netns` as the port driver (see also [this guide](https://rootlesscontaine.rs/getting-started/docker/#changing-the-port-forwarder)). +As stated in the documentation, this change will likely lead to decreased network throughput. You should test this by trying to transfer a large file after completing your setup and revert back to the `builtin` port driver if the throughput is too slow. +* Add `net.ipv4.ip_unprivileged_port_start=80` to `/etc/sysctl.conf`. Editing this file requires root privileges. (using capabilities doesn't work here; see [this issue](https://github.com/rootless-containers/slirp4netns/issues/251#issuecomment-761415404)). +* Run `sudo sysctl --system` to propagate the change. +* Create `~/.config/systemd/user/docker.service.d/override.conf` + with the following content: + ``` + [Service] + Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns" + Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns" + ``` +* Restart the docker daemon + ``` + systemctl --user restart docker + ``` diff --git a/local-instance.md b/local-instance.md index ffc6380e..8abbddb6 100644 --- a/local-instance.md +++ b/local-instance.md @@ -1,19 +1,32 @@ # Local instance -It is possible due to several reasons that you do not want or cannot open Nextcloud to the public internet. However AIO requires a valid certificate to work correctly. Below is discussed how you can achieve both: Having a valid certificate for Nextcloud and only using it locally. +It is possible due to several reasons that you do not want or cannot open Nextcloud to the public internet. Perhaps you were hoping to access AIO directly from an `ip.add.r.ess` (unsupported) or without a valid domain. However, AIO requires a valid certificate to work correctly. Below is discussed how you can achieve both: Having a valid certificate for Nextcloud and only using it locally. -## 1. The recommended way -The recommended way is the following: +### Content +- [1. Tailscale](#1-tailscale) +- [2. The normal way](#2-the-normal-way) +- [3. Use the ACME DNS-challenge](#3-use-the-acme-dns-challenge) +- [4. Use Cloudflare](#4-use-cloudflare) +- [5. Buy a certificate and use that](#5-buy-a-certificate-and-use-that) + +## 1. Tailscale +This is the recommended way. For a reverse proxy example guide for Tailscale, see this guide by [@Perseus333](https://github.com/Perseus333): https://github.com/nextcloud/all-in-one/discussions/6817 + +## 2. The normal way +The normal way is the following: 1. Set up your domain correctly to point to your home network 1. Set up a reverse proxy by following the [reverse proxy documentation](./reverse-proxy.md) but only open port 80 (which is needed for the ACME challenge to work - however no real traffic will use this port). -1. Set up a local DNS-server like a pi-hole and configure it to be your local DNS-server for the whole network. Then in the Pi-hole interface, add a custom DNS-record for your domain and overwrite the A-record (and possibly the AAAA-record, too) to point to the local ip-address of your reverse proxy -1. Enter the the ip-address of your local dns-server in the deamon.json file for docker so that you are sure that all docker containers use the correct local dns-server. +1. Set up a local DNS-server like a pi-hole and configure it to be your local DNS-server for the whole network. Then in the Pi-hole interface, add a custom DNS-record for your domain and overwrite the A-record (and possibly the AAAA-record, too) to point to the private ip-address of your reverse proxy (see https://github.com/nextcloud/all-in-one#how-can-i-access-nextcloud-locally) +1. Enter the ip-address of your local dns-server in the daemon.json file for docker so that you are sure that all docker containers use the correct local dns-server. 1. Now, entering the domain in the AIO-interface should work as expected and should allow you to continue with the setup -## 2. Use the ACME DNS-challenge -You can alternatively use the ACME DNS-challenge to get a valid certificate for Nextcloud. Here is described how to set it up: https://github.com/nextcloud/all-in-one#how-to-get-nextcloud-running-using-the-acme-dns-challenge +**Hint:** You may have a look at [this video](https://youtu.be/zk-y2wVkY4c) for a more complete but possibly outdated example. -## 3. Use Cloudflare -If you do not have any contol over the network, you may think about using Cloudflare Argo Tunnel to get a valid certificate for your Nextcloud. However it will be opened to the public internet then. See https://github.com/nextcloud/all-in-one#how-to-run-nextcloud-behind-a-cloudflare-argo-tunnel how to set this up. +## 3. Use the ACME DNS-challenge +You can alternatively use the ACME DNS-challenge to get a valid certificate for Nextcloud. Here is described how to set it up using an external caddy reverse proxy: https://github.com/nextcloud/all-in-one#how-to-get-nextcloud-running-using-the-acme-dns-challenge -## 4. Buy a certificate and use that +## 4. Use Cloudflare +If you do not have any control over the network, you may think about using Cloudflare Tunnel to get a valid certificate for your Nextcloud. However it will be opened to the public internet then. See https://github.com/nextcloud/all-in-one#how-to-run-nextcloud-behind-a-cloudflare-tunnel how to set this up. + +## 5. Buy a certificate and use that If none of the above ways work for you, you may simply buy a certificate from an issuer for your domain. You then download the certificate onto your server, configure AIO in [reverse proxy mode](./reverse-proxy.md) and use the certificate for your domain in your reverse proxy config. + diff --git a/manual-install/latest-arm64.yml b/manual-install/latest-arm64.yml deleted file mode 100644 index 7649fe38..00000000 --- a/manual-install/latest-arm64.yml +++ /dev/null @@ -1,139 +0,0 @@ -version: "3.8" - -services: - nextcloud-aio-apache: - container_name: nextcloud-aio-apache - depends_on: - - nextcloud-aio-collabora - - nextcloud-aio-talk - - nextcloud-aio-nextcloud - image: nextcloud/aio-apache:latest-arm64 - ports: - - ${APACHE_PORT}:${APACHE_PORT}/tcp - environment: - - NC_DOMAIN=${NC_DOMAIN} - - NEXTCLOUD_HOST=nextcloud-aio-nextcloud - - COLLABORA_HOST=nextcloud-aio-collabora - - TALK_HOST=nextcloud-aio-talk - - APACHE_PORT=${APACHE_PORT} - - TZ=${TIMEZONE} - volumes: - - nextcloud_aio_nextcloud:/var/www/html:ro - - nextcloud_aio_apache:/mnt/data:rw - stop_grace_period: 10s - restart: unless-stopped - networks: - - nextcloud-aio - - nextcloud-aio-database: - container_name: nextcloud-aio-database - image: nextcloud/aio-postgresql:latest-arm64 - volumes: - - nextcloud_aio_database:/var/lib/postgresql/data:rw - - nextcloud_aio_database_dump:/mnt/data:rw - environment: - - POSTGRES_PASSWORD=${DATABASE_PASSWORD} - - POSTGRES_DB=nextcloud_database - - POSTGRES_USER=nextcloud - - TZ=${TIMEZONE} - - PGTZ=${TIMEZONE} - stop_grace_period: 1800s - restart: unless-stopped - networks: - - nextcloud-aio - - nextcloud-aio-nextcloud: - container_name: nextcloud-aio-nextcloud - depends_on: - - nextcloud-aio-database - - nextcloud-aio-redis - image: nextcloud/aio-nextcloud:latest-arm64 - volumes: - - nextcloud_aio_nextcloud:/var/www/html:rw - - ${NEXTCLOUD_DATADIR}:/mnt/ncdata:rw - - ${NEXTCLOUD_MOUNT}:${NEXTCLOUD_MOUNT}:rw - environment: - - POSTGRES_HOST=nextcloud-aio-database - - POSTGRES_PASSWORD=${DATABASE_PASSWORD} - - POSTGRES_DB=nextcloud_database - - POSTGRES_USER=nextcloud - - REDIS_HOST=nextcloud-aio-redis - - REDIS_HOST_PASSWORD=${REDIS_PASSWORD} - - AIO_TOKEN=${AIO_TOKEN} - - NC_DOMAIN=${NC_DOMAIN} - - ADMIN_USER=admin - - ADMIN_PASSWORD=${NEXTCLOUD_PASSWORD} - - NEXTCLOUD_DATA_DIR=/mnt/ncdata - - OVERWRITEHOST=${NC_DOMAIN} - - OVERWRITEPROTOCOL=https - - TURN_SECRET=${TURN_SECRET} - - SIGNALING_SECRET=${SIGNALING_SECRET} - - AIO_URL=${AIO_URL} - - NEXTCLOUD_MOUNT=${NEXTCLOUD_MOUNT} - - COLLABORA_ENABLED=${COLLABORA_ENABLED} - - COLLABORA_HOST=nextcloud-aio-collabora - - TALK_ENABLED=${TALK_ENABLED} - - UPDATE_NEXTCLOUD_APPS=${UPDATE_NEXTCLOUD_APPS} - - TZ=${TIMEZONE} - - TALK_PORT=${TALK_PORT} - stop_grace_period: 10s - restart: unless-stopped - networks: - - nextcloud-aio - - nextcloud-aio-redis: - container_name: nextcloud-aio-redis - image: nextcloud/aio-redis:latest-arm64 - environment: - - REDIS_HOST_PASSWORD=${REDIS_PASSWORD} - - TZ=${TIMEZONE} - stop_grace_period: 10s - restart: unless-stopped - networks: - - nextcloud-aio - - nextcloud-aio-collabora: - container_name: nextcloud-aio-collabora - image: nextcloud/aio-collabora:latest-arm64 - environment: - - aliasgroup1=https://${NC_DOMAIN}:443 - - extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:logging.level=warning --o:home_mode.enable=true - - dictionaries=${COLLABORA_DICTIONARIES} - - TZ=${TIMEZONE} - stop_grace_period: 10s - restart: unless-stopped - networks: - - nextcloud-aio - - nextcloud-aio-talk: - container_name: nextcloud-aio-talk - image: nextcloud/aio-talk:latest-arm64 - ports: - - ${TALK_PORT}:${TALK_PORT}/tcp - - ${TALK_PORT}:${TALK_PORT}/udp - environment: - - NC_DOMAIN=${NC_DOMAIN} - - TURN_SECRET=${TURN_SECRET} - - SIGNALING_SECRET=${SIGNALING_SECRET} - - JANUS_API_KEY=${JANUS_API_KEY} - - TZ=${TIMEZONE} - - TALK_PORT=${TALK_PORT} - stop_grace_period: 10s - restart: unless-stopped - networks: - - nextcloud-aio - -volumes: - nextcloud_aio_apache: - name: nextcloud_aio_apache - nextcloud_aio_database: - name: nextcloud_aio_database - nextcloud_aio_database_dump: - name: nextcloud_aio_database_dump - nextcloud_aio_nextcloud: - name: nextcloud_aio_nextcloud - nextcloud_aio_nextcloud_data: - name: nextcloud_aio_nextcloud_data - -networks: - nextcloud-aio: diff --git a/manual-install/latest.yml b/manual-install/latest.yml index d7ce9797..e9362ccc 100644 --- a/manual-install/latest.yml +++ b/manual-install/latest.yml @@ -1,36 +1,77 @@ -version: "3.8" - services: nextcloud-aio-apache: - container_name: nextcloud-aio-apache depends_on: - - nextcloud-aio-onlyoffice - - nextcloud-aio-collabora - - nextcloud-aio-clamav - - nextcloud-aio-talk - - nextcloud-aio-nextcloud - image: nextcloud/aio-apache:latest + nextcloud-aio-onlyoffice: + condition: service_started + required: false + nextcloud-aio-collabora: + condition: service_started + required: false + nextcloud-aio-talk: + condition: service_started + required: false + nextcloud-aio-notify-push: + condition: service_started + required: false + nextcloud-aio-whiteboard: + condition: service_started + required: false + nextcloud-aio-nextcloud: + condition: service_started + required: false + image: ghcr.io/nextcloud-releases/aio-apache:latest + user: "33" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 ports: - - ${APACHE_PORT}:${APACHE_PORT}/tcp + - ${APACHE_IP_BINDING}:${APACHE_PORT}:${APACHE_PORT}/tcp + - ${APACHE_IP_BINDING}:${APACHE_PORT}:${APACHE_PORT}/udp environment: - - NC_DOMAIN=${NC_DOMAIN} + - NC_DOMAIN - NEXTCLOUD_HOST=nextcloud-aio-nextcloud + - APACHE_HOST=nextcloud-aio-apache - COLLABORA_HOST=nextcloud-aio-collabora - TALK_HOST=nextcloud-aio-talk - - APACHE_PORT=${APACHE_PORT} + - APACHE_PORT - ONLYOFFICE_HOST=nextcloud-aio-onlyoffice - TZ=${TIMEZONE} + - APACHE_MAX_SIZE + - APACHE_MAX_TIME=${NEXTCLOUD_MAX_TIME} + - NOTIFY_PUSH_HOST=nextcloud-aio-notify-push + - WHITEBOARD_HOST=nextcloud-aio-whiteboard volumes: - nextcloud_aio_nextcloud:/var/www/html:ro - nextcloud_aio_apache:/mnt/data:rw - stop_grace_period: 10s restart: unless-stopped - networks: - - nextcloud-aio - + read_only: true + tmpfs: + - /var/log/supervisord + - /var/run/supervisord + - /usr/local/apache2/logs + - /tmp + - /home/www-data + cap_drop: + - NET_RAW + nextcloud-aio-database: - container_name: nextcloud-aio-database - image: nextcloud/aio-postgresql:latest + image: ghcr.io/nextcloud-releases/aio-postgresql:latest + user: "999" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "5432" volumes: - nextcloud_aio_database:/var/lib/postgresql/data:rw - nextcloud_aio_database_dump:/mnt/data:rw @@ -42,110 +83,309 @@ services: - PGTZ=${TIMEZONE} stop_grace_period: 1800s restart: unless-stopped - networks: - - nextcloud-aio - + shm_size: 268435456 + read_only: true + tmpfs: + - /var/run/postgresql + cap_drop: + - NET_RAW + nextcloud-aio-nextcloud: - container_name: nextcloud-aio-nextcloud depends_on: - - nextcloud-aio-database - - nextcloud-aio-redis - image: nextcloud/aio-nextcloud:latest + nextcloud-aio-database: + condition: service_started + required: false + nextcloud-aio-redis: + condition: service_started + required: false + nextcloud-aio-clamav: + condition: service_started + required: false + nextcloud-aio-fulltextsearch: + condition: service_started + required: false + nextcloud-aio-talk-recording: + condition: service_started + required: false + nextcloud-aio-imaginary: + condition: service_started + required: false + image: ghcr.io/nextcloud-releases/aio-nextcloud:latest + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "9000" + - "9001" volumes: - nextcloud_aio_nextcloud:/var/www/html:rw - ${NEXTCLOUD_DATADIR}:/mnt/ncdata:rw - ${NEXTCLOUD_MOUNT}:${NEXTCLOUD_MOUNT}:rw + - ${NEXTCLOUD_TRUSTED_CACERTS_DIR}:/usr/local/share/ca-certificates:ro environment: + - NEXTCLOUD_HOST=nextcloud-aio-nextcloud - POSTGRES_HOST=nextcloud-aio-database + - POSTGRES_PORT=5432 - POSTGRES_PASSWORD=${DATABASE_PASSWORD} - POSTGRES_DB=nextcloud_database - POSTGRES_USER=nextcloud - REDIS_HOST=nextcloud-aio-redis + - REDIS_PORT=6379 - REDIS_HOST_PASSWORD=${REDIS_PASSWORD} - - AIO_TOKEN=${AIO_TOKEN} - - NC_DOMAIN=${NC_DOMAIN} + - APACHE_HOST=nextcloud-aio-apache + - APACHE_PORT + - NC_DOMAIN - ADMIN_USER=admin - ADMIN_PASSWORD=${NEXTCLOUD_PASSWORD} - NEXTCLOUD_DATA_DIR=/mnt/ncdata - OVERWRITEHOST=${NC_DOMAIN} - OVERWRITEPROTOCOL=https - - TURN_SECRET=${TURN_SECRET} - - SIGNALING_SECRET=${SIGNALING_SECRET} - - ONLYOFFICE_SECRET=${ONLYOFFICE_SECRET} - - AIO_URL=${AIO_URL} - - NEXTCLOUD_MOUNT=${NEXTCLOUD_MOUNT} - - CLAMAV_ENABLED=${CLAMAV_ENABLED} + - TURN_SECRET + - SIGNALING_SECRET + - ONLYOFFICE_SECRET + - NEXTCLOUD_MOUNT + - CLAMAV_ENABLED - CLAMAV_HOST=nextcloud-aio-clamav - - ONLYOFFICE_ENABLED=${ONLYOFFICE_ENABLED} - - COLLABORA_ENABLED=${COLLABORA_ENABLED} + - ONLYOFFICE_ENABLED + - COLLABORA_ENABLED - COLLABORA_HOST=nextcloud-aio-collabora - - TALK_ENABLED=${TALK_ENABLED} + - TALK_ENABLED - ONLYOFFICE_HOST=nextcloud-aio-onlyoffice - - UPDATE_NEXTCLOUD_APPS=${UPDATE_NEXTCLOUD_APPS} + - UPDATE_NEXTCLOUD_APPS - TZ=${TIMEZONE} - - TALK_PORT=${TALK_PORT} - stop_grace_period: 10s + - TALK_PORT + - IMAGINARY_ENABLED + - IMAGINARY_HOST=nextcloud-aio-imaginary + - PHP_UPLOAD_LIMIT=${NEXTCLOUD_UPLOAD_LIMIT} + - PHP_MEMORY_LIMIT=${NEXTCLOUD_MEMORY_LIMIT} + - FULLTEXTSEARCH_ENABLED + - FULLTEXTSEARCH_HOST=nextcloud-aio-fulltextsearch + - FULLTEXTSEARCH_PROTOCOL=http + - FULLTEXTSEARCH_PORT=9200 + - FULLTEXTSEARCH_USER=elastic + - FULLTEXTSEARCH_INDEX=nextcloud-aio + - PHP_MAX_TIME=${NEXTCLOUD_MAX_TIME} + - TRUSTED_CACERTS_DIR=${NEXTCLOUD_TRUSTED_CACERTS_DIR} + - STARTUP_APPS=${NEXTCLOUD_STARTUP_APPS} + - ADDITIONAL_APKS=${NEXTCLOUD_ADDITIONAL_APKS} + - ADDITIONAL_PHP_EXTENSIONS=${NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS} + - INSTALL_LATEST_MAJOR + - TALK_RECORDING_ENABLED + - RECORDING_SECRET + - TALK_RECORDING_HOST=nextcloud-aio-talk-recording + - FULLTEXTSEARCH_PASSWORD + - REMOVE_DISABLED_APPS + - IMAGINARY_SECRET + - WHITEBOARD_SECRET + - WHITEBOARD_ENABLED + stop_grace_period: 600s restart: unless-stopped - networks: - - nextcloud-aio - + cap_drop: + - NET_RAW + + nextcloud-aio-notify-push: + image: ghcr.io/nextcloud-releases/aio-notify-push:latest + user: "33" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "7867" + volumes: + - nextcloud_aio_nextcloud:/nextcloud:ro + environment: + - NC_DOMAIN + - NEXTCLOUD_HOST=nextcloud-aio-nextcloud + - TZ=${TIMEZONE} + - REDIS_HOST=nextcloud-aio-redis + - REDIS_PORT=6379 + - REDIS_HOST_PASSWORD=${REDIS_PASSWORD} + - POSTGRES_HOST=nextcloud-aio-database + - POSTGRES_PORT=5432 + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + - POSTGRES_DB=nextcloud_database + - POSTGRES_USER=nextcloud + restart: unless-stopped + read_only: true + cap_drop: + - NET_RAW + nextcloud-aio-redis: - container_name: nextcloud-aio-redis - image: nextcloud/aio-redis:latest + image: ghcr.io/nextcloud-releases/aio-redis:latest + user: "999" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "6379" environment: - REDIS_HOST_PASSWORD=${REDIS_PASSWORD} - TZ=${TIMEZONE} - stop_grace_period: 10s + volumes: + - nextcloud_aio_redis:/data:rw restart: unless-stopped - networks: - - nextcloud-aio - + read_only: true + cap_drop: + - NET_RAW + nextcloud-aio-collabora: - container_name: nextcloud-aio-collabora - image: nextcloud/aio-collabora:latest + command: ${ADDITIONAL_COLLABORA_OPTIONS} + image: ghcr.io/nextcloud-releases/aio-collabora:latest + init: true + healthcheck: + start_period: 60s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 9 + expose: + - "9980" environment: - - aliasgroup1=https://${NC_DOMAIN}:443 - - extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:logging.level=warning --o:home_mode.enable=true + - aliasgroup1=https://${NC_DOMAIN}:443,http://nextcloud-aio-apache:23973 + - extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:logging.disable_server_audit=true --o:logging.level=warning --o:logging.level_startup=warning --o:welcome.enable=false --o:remote_font_config.url=https://${NC_DOMAIN}/apps/richdocuments/settings/fonts.json --o:net.post_allow.host[0]=.+ - dictionaries=${COLLABORA_DICTIONARIES} - TZ=${TIMEZONE} - stop_grace_period: 10s + - server_name=${NC_DOMAIN} + - DONT_GEN_SSL_CERT=1 restart: unless-stopped - networks: - - nextcloud-aio - + profiles: + - collabora + cap_add: + - MKNOD + - SYS_ADMIN + - SYS_CHROOT + - FOWNER + - CHOWN + cap_drop: + - NET_RAW + nextcloud-aio-talk: - container_name: nextcloud-aio-talk - image: nextcloud/aio-talk:latest + image: ghcr.io/nextcloud-releases/aio-talk:latest + user: "1000" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 ports: - ${TALK_PORT}:${TALK_PORT}/tcp - ${TALK_PORT}:${TALK_PORT}/udp + expose: + - "8081" environment: - - NC_DOMAIN=${NC_DOMAIN} - - TURN_SECRET=${TURN_SECRET} - - SIGNALING_SECRET=${SIGNALING_SECRET} - - JANUS_API_KEY=${JANUS_API_KEY} + - NC_DOMAIN + - TALK_HOST=nextcloud-aio-talk + - TURN_SECRET + - SIGNALING_SECRET - TZ=${TIMEZONE} - - TALK_PORT=${TALK_PORT} - stop_grace_period: 10s + - TALK_PORT + - INTERNAL_SECRET=${TALK_INTERNAL_SECRET} restart: unless-stopped - networks: - - nextcloud-aio - + profiles: + - talk + - talk-recording + read_only: true + tmpfs: + - /var/log/supervisord + - /var/run/supervisord + - /opt/eturnal/run + - /conf + - /tmp + cap_drop: + - NET_RAW + + nextcloud-aio-talk-recording: + image: ghcr.io/nextcloud-releases/aio-talk-recording:latest + user: "122" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "1234" + environment: + - NC_DOMAIN + - TZ=${TIMEZONE} + - RECORDING_SECRET + - INTERNAL_SECRET=${TALK_INTERNAL_SECRET} + volumes: + - nextcloud_aio_talk_recording:/tmp:rw + shm_size: 2147483648 + restart: unless-stopped + profiles: + - talk-recording + read_only: true + tmpfs: + - /conf + cap_drop: + - NET_RAW + nextcloud-aio-clamav: - container_name: nextcloud-aio-clamav - image: nextcloud/aio-clamav:latest + image: ghcr.io/nextcloud-releases/aio-clamav:latest + user: "100" + init: false + healthcheck: + start_period: 60s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 9 + expose: + - "3310" environment: - TZ=${TIMEZONE} + - MAX_SIZE=${NEXTCLOUD_UPLOAD_LIMIT} volumes: - nextcloud_aio_clamav:/var/lib/clamav:rw - stop_grace_period: 10s restart: unless-stopped - networks: - - nextcloud-aio - + profiles: + - clamav + read_only: true + tmpfs: + - /tmp + - /var/log/clamav + - /run/clamav + - /var/log/supervisord + - /var/run/supervisord + cap_drop: + - NET_RAW + nextcloud-aio-onlyoffice: - container_name: nextcloud-aio-onlyoffice - image: nextcloud/aio-onlyoffice:latest + image: ghcr.io/nextcloud-releases/aio-onlyoffice:latest + init: true + healthcheck: + start_period: 60s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 9 + expose: + - "80" environment: - TZ=${TIMEZONE} - JWT_ENABLED=true @@ -153,10 +393,100 @@ services: - JWT_SECRET=${ONLYOFFICE_SECRET} volumes: - nextcloud_aio_onlyoffice:/var/lib/onlyoffice:rw - stop_grace_period: 10s restart: unless-stopped - networks: - - nextcloud-aio + profiles: + - onlyoffice + cap_drop: + - NET_RAW + + nextcloud-aio-imaginary: + image: ghcr.io/nextcloud-releases/aio-imaginary:latest + user: "65534" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "9000" + environment: + - TZ=${TIMEZONE} + - IMAGINARY_SECRET + restart: unless-stopped + cap_add: + - SYS_NICE + cap_drop: + - NET_RAW + profiles: + - imaginary + read_only: true + tmpfs: + - /tmp + + nextcloud-aio-fulltextsearch: + image: ghcr.io/nextcloud-releases/aio-fulltextsearch:latest + init: false + healthcheck: + start_period: 60s + test: /healthcheck.sh + interval: 10s + timeout: 5s + start_interval: 5s + retries: 5 + expose: + - "9200" + environment: + - TZ=${TIMEZONE} + - ES_JAVA_OPTS=${FULLTEXTSEARCH_JAVA_OPTIONS} + - bootstrap.memory_lock=false + - cluster.name=nextcloud-aio + - discovery.type=single-node + - logger.level=WARN + - http.port=9200 + - xpack.license.self_generated.type=basic + - xpack.security.enabled=false + - FULLTEXTSEARCH_PASSWORD + volumes: + - nextcloud_aio_elasticsearch:/usr/share/elasticsearch/data:rw + restart: unless-stopped + profiles: + - fulltextsearch + cap_drop: + - NET_RAW + + nextcloud-aio-whiteboard: + image: ghcr.io/nextcloud-releases/aio-whiteboard:latest + user: "65534" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "3002" + tmpfs: + - /tmp + environment: + - TZ=${TIMEZONE} + - NEXTCLOUD_URL=https://${NC_DOMAIN} + - JWT_SECRET_KEY=${WHITEBOARD_SECRET} + - STORAGE_STRATEGY=redis + - REDIS_HOST=nextcloud-aio-redis + - REDIS_PORT=6379 + - REDIS_HOST_PASSWORD=${REDIS_PASSWORD} + - BACKUP_DIR=/tmp + restart: unless-stopped + profiles: + - whiteboard + read_only: true + cap_drop: + - NET_RAW volumes: nextcloud_aio_apache: @@ -167,12 +497,19 @@ volumes: name: nextcloud_aio_database nextcloud_aio_database_dump: name: nextcloud_aio_database_dump + nextcloud_aio_elasticsearch: + name: nextcloud_aio_elasticsearch nextcloud_aio_nextcloud: name: nextcloud_aio_nextcloud nextcloud_aio_onlyoffice: name: nextcloud_aio_onlyoffice + nextcloud_aio_redis: + name: nextcloud_aio_redis + nextcloud_aio_talk_recording: + name: nextcloud_aio_talk_recording nextcloud_aio_nextcloud_data: name: nextcloud_aio_nextcloud_data networks: - nextcloud-aio: + default: + driver: bridge diff --git a/manual-install/readme.md b/manual-install/readme.md index 24efebca..ea2c2978 100644 --- a/manual-install/readme.md +++ b/manual-install/readme.md @@ -5,34 +5,47 @@ You can run the containers that are build for AIO with docker-compose. This come ### Advantages - You can run it without a container having access to the docker socket - You can modify all values on your own +- You can run the containers with docker swarm +- You can run this in environments where access to ghcr.io is not possible. See [this issue](https://github.com/nextcloud/all-in-one/discussions/5268). ### Disadvantages - You lose the AIO interface - You lose update notifications and automatic updates - You lose all AIO backup and restore features -- You need to know what you are doing, especially when modifying the docker-compose file +- You lose the built-in [Docker Socket Proxy container](https://github.com/nextcloud/docker-socket-proxy#readme) (needed for [Nextcloud App API](https://github.com/nextcloud/app_api#nextcloud-appapi)) +- You lose all community containers: https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers +- **You need to know what you are doing, especially when modifying the compose.yaml file** +- For updating, you need to strictly follow the at the bottom described update routine - Probably more ## How to use this? -First, install docker and docker-compose if not already done. Then simply run the following: +First, install docker and docker-compose (v2) if not already done. Then simply run the following: ```bash git clone https://github.com/nextcloud/all-in-one.git cd all-in-one/manual-install ``` -Then copy the sample.conf to a new file, e.g. `cp sample.conf my.conf`, open the new conf file, e.g. with `nano my.conf`, edit all values that are marked with `# TODO!`, close and save the file. +Then copy the sample.conf to default environment file, e.g. `cp sample.conf .env`, open the new conf file, e.g. with `nano .env`, edit all values that are marked with `# TODO!`, close and save the file.
+⚠️ **Warning**: Do not use the symbols `@` and `:` in your passwords. These symbols are used to build database connection strings. You will experience issues when using these symbols! Also please note that values inside the latest.yaml that are not exposed as variables are not officially supported to be changed. See for example [this report](https://github.com/nextcloud/all-in-one/issues/5612). -Now copy the provided yaml file to a docker-compose file by running on x64 `cp latest.yml docker-compose.yml` and on arm64 `cp latest-arm64.yml docker-compose.yml`. +Now copy the provided yaml file to a compose.yaml file by running `cp latest.yml compose.yaml`. -Now you should be ready to go with `sudo docker-compose --env-file my.conf up`. +Now you should be ready to go with `sudo docker compose up`. + +## Docker profiles +The default profile of `latest.yml` only provide the minimum necessary services: nextcloud, database, redis and apache. To get optional services collabora, talk, whiteboard, talk-recording, clamav, imaginary or fulltextsearch use additional arguments for each of them, for example `--profile collabora`. + +For a complete all-in-one with collabora use `sudo docker compose --profile collabora --profile talk --profile talk-recording --profile clamav --profile imaginary --profile fulltextsearch --profile whiteboard up`. ## How to update? Since the AIO containers may change in the future, it is highly recommended to strictly follow the following procedure whenever you want to upgrade your containers. -1. Run `sudo docker-compose --env-file my.conf down` to stop all running containers +1. If your previous copy of `sample.conf` is named `my.conf`, run `mv -vn my.conf .env` in order to rename the file to `.env`. +1. Run `sudo docker compose down` to stop all running containers 1. Back up all important files and folders -1. Run `git pull` in order to get the updated yaml files from the repository. Now bring your `docker-compose.yml` file up-to-date with the updated one from the repository. You can use `diff docker-compose.yml latest.yml` on x64 and `diff docker-compose.yml latest-arm64.yml` on arm64 for comparing. +1. If your compose file is still named `docker-compose.yml` rename it to `compose.yaml` by running `mv -vn docker-compose.yml compose.yaml` +1. Run `git pull` in order to get the updated yaml files from the repository. Now bring your `compose.yaml` file up-to-date with the updated one from the repository. You can use `diff compose.yaml latest.yml` for comparing. ⚠️ **Please note**: Starting with AIO v5.1.0, ipv6 networking will be enabled by default, so make sure to either enable it first by following steps 1 and 2 of https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md and then proceed with the steps below or disable ipv6 networking by editing the compose.yaml file and removing ipv6 from the network. 1. Also have a look at the `sample.conf` if any variable was added or renamed and add that to your conf file as well. Here may help the diff command as well. -1. After the file update was successful, simply run `sudo docker-compose --env-file my.conf pull` to pull the new images. -1. At the end run `sudo docker-compose --env-file my.conf up` in order to start and update the containers with the new configuration. +1. After the file update was successful, simply run `sudo docker compose pull` to pull the new images. +1. At the end run `sudo docker compose up` in order to start and update the containers with the new configuration. ## FAQ ### Backup and restore? diff --git a/manual-install/sample.conf b/manual-install/sample.conf index 4aadd825..9ee01ab1 100644 --- a/manual-install/sample.conf +++ b/manual-install/sample.conf @@ -1,21 +1,42 @@ -AIO_TOKEN=123456 # Has no function but needs to be set! -AIO_URL=localhost # Has no function but needs to be set! -APACHE_PORT=443 # Changing this to a different value than 443 will allow you to run it behind a reverse proxy. -CLAMAV_ENABLED=no # Setting this to "yes" enables the option in Nextcloud automatically. -COLLABORA_DICTIONARIES=de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru # You can change this in order to enable other dictionaries for collabora -COLLABORA_ENABLED=yes # Setting this to "yes" enables the option in Nextcloud automatically. DATABASE_PASSWORD= # TODO! This needs to be a unique and good password! -JANUS_API_KEY= # TODO! This needs to be a unique and good password! +FULLTEXTSEARCH_PASSWORD= # TODO! This needs to be a unique and good password! +IMAGINARY_SECRET= # TODO! This needs to be a unique and good password! NC_DOMAIN=yourdomain.com # TODO! Needs to be changed to the domain that you want to use for Nextcloud. -NEXTCLOUD_DATADIR=nextcloud_aio_nextcloud_data # You can change this to e.g. "/mnt/ncdata" to map it to a location on your host. It needs to be adjusted before the first startup and never afterwards! -NEXTCLOUD_MOUNT=/mnt/ # This allows the Nextcloud container to access directories on the host. It must never be equal to the value of NEXTCLOUD_DATADIR! NEXTCLOUD_PASSWORD= # TODO! This is the password of the initially created Nextcloud admin with username "admin". -ONLYOFFICE_ENABLED=no # Setting this to "yes" enables the option in Nextcloud automatically. ONLYOFFICE_SECRET= # TODO! This needs to be a unique and good password! +RECORDING_SECRET= # TODO! This needs to be a unique and good password! REDIS_PASSWORD= # TODO! This needs to be a unique and good password! SIGNALING_SECRET= # TODO! This needs to be a unique and good password! -TALK_ENABLED=yes # Setting this to "yes" enables the option in Nextcloud automatically. -TALK_PORT=3478 # This allows to adjust the port that the talk container is using. +TALK_INTERNAL_SECRET= # TODO! This needs to be a unique and good password! TIMEZONE=Europe/Berlin # TODO! This is the timezone that your containers will use. TURN_SECRET= # TODO! This needs to be a unique and good password! -UPDATE_NEXTCLOUD_APPS=no # When setting to yes, it will automatically update all installed Nextcloud apps upon container startup on saturdays. +WHITEBOARD_SECRET= # TODO! This needs to be a unique and good password! + +CLAMAV_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +COLLABORA_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +FULLTEXTSEARCH_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +IMAGINARY_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +ONLYOFFICE_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +TALK_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +TALK_RECORDING_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +WHITEBOARD_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. + +APACHE_IP_BINDING=0.0.0.0 # This can be changed to e.g. 127.0.0.1 if you want to run AIO behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) and if that is running on the same host and using localhost to connect +APACHE_MAX_SIZE=17179869184 # This needs to be an integer and in sync with NEXTCLOUD_UPLOAD_LIMIT +APACHE_PORT=443 # Changing this to a different value than 443 will allow you to run it behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). +ADDITIONAL_COLLABORA_OPTIONS=['--o:security.seccomp=true'] # You can add additional collabora options here by using the array syntax. +COLLABORA_DICTIONARIES="de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru" # You can change this in order to enable other dictionaries for collabora +FULLTEXTSEARCH_JAVA_OPTIONS="-Xms512M -Xmx512M" # Allows to adjust the fulltextsearch java options. +INSTALL_LATEST_MAJOR=no # Setting this to yes will install the latest Major Nextcloud version upon the first installation +NEXTCLOUD_ADDITIONAL_APKS=imagemagick # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value. +NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS=imagick # This allows to add additional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value. +NEXTCLOUD_DATADIR=nextcloud_aio_nextcloud_data # You can change this to e.g. "/mnt/ncdata" to map it to a location on your host. It needs to be adjusted before the first startup and never afterwards! +NEXTCLOUD_MAX_TIME=3600 # This allows to change the upload time limit of the Nextcloud container +NEXTCLOUD_MEMORY_LIMIT=512M # This allows to change the PHP memory limit of the Nextcloud container +NEXTCLOUD_MOUNT=/mnt/ # This allows the Nextcloud container to access directories on the host. It must never be equal to the value of NEXTCLOUD_DATADIR! +NEXTCLOUD_STARTUP_APPS="deck twofactor_totp tasks calendar contacts notes" # Allows to modify the Nextcloud apps that are installed on starting AIO the first time +NEXTCLOUD_TRUSTED_CACERTS_DIR=/usr/local/share/ca-certificates/my-custom-ca # Nextcloud container will trust all the Certification Authorities, whose certificates are included in the given directory. +NEXTCLOUD_UPLOAD_LIMIT=16G # This allows to change the upload limit of the Nextcloud container +REMOVE_DISABLED_APPS=yes # Setting this to no keep Nextcloud apps that are disabled via their switch and not uninstall them if they should be installed in Nextcloud. +TALK_PORT=3478 # This allows to adjust the port that the talk container is using. It should be set to something higher than 1024! Otherwise it might not work! +UPDATE_NEXTCLOUD_APPS="no" # When setting to "yes" (with quotes), it will automatically update all installed Nextcloud apps upon container startup on saturdays. diff --git a/manual-install/update-yaml.sh b/manual-install/update-yaml.sh index 3cf23451..928275da 100644 --- a/manual-install/update-yaml.sh +++ b/manual-install/update-yaml.sh @@ -1,40 +1,60 @@ -#!/bin/bash +#!/bin/bash -ex + +type {jq,sudo} || { echo "Commands not found. Please install them"; exit 127; } jq -c . ./php/containers.json > /tmp/containers.json -sed -i 's|","location":"|:|g' /tmp/containers.json +sed -i 's|aio_services_v1|services|g' /tmp/containers.json +sed -i 's|","destination":"|:|g' /tmp/containers.json sed -i 's|","writeable":false|:ro"|g' /tmp/containers.json sed -i 's|","writeable":true|:rw"|g' /tmp/containers.json +sed -i 's|","port_number":"|:|g' /tmp/containers.json +sed -i 's|","protocol":"|/|g' /tmp/containers.json +sed -i 's|"ip_binding":":|"ip_binding":"|g' /tmp/containers.json +cat /tmp/containers.json OUTPUT="$(cat /tmp/containers.json)" -OUTPUT="$(echo "$OUTPUT" | jq 'del(.production[].internalPorts)')" -OUTPUT="$(echo "$OUTPUT" | jq 'del(.production[].secrets)')" -OUTPUT="$(echo "$OUTPUT" | jq 'del(.production[] | select(.identifier == "nextcloud-aio-watchtower"))')" -OUTPUT="$(echo "$OUTPUT" | jq 'del(.production[] | select(.identifier == "nextcloud-aio-domaincheck"))')" -OUTPUT="$(echo "$OUTPUT" | jq 'del(.production[] | select(.identifier == "nextcloud-aio-borgbackup"))')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].internal_port)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].secrets)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].ui_secrets)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].devices)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].enable_nvidia_gpu)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].backup_volumes)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].nextcloud_exec_commands)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].image_tag)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].networks)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].documentation)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-watchtower"))')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-domaincheck"))')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-borgbackup"))')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-docker-socket-proxy"))')" +OUTPUT="$(echo "$OUTPUT" | jq '.services[] |= if has("depends_on") then .depends_on |= if contains(["nextcloud-aio-docker-socket-proxy"]) then del(.[index("nextcloud-aio-docker-socket-proxy")]) else . end else . end')" +OUTPUT="$(echo "$OUTPUT" | jq '.services[] |= if has("depends_on") then .depends_on |= map({ (.): { "condition": "service_started", "required": false } }) else . end' | jq '.services[] |= if has("depends_on") then .depends_on |= reduce .[] as $item ({}; . + $item) else . end')" -snap install yq +sudo snap install yq mkdir -p ./manual-install echo "$OUTPUT" | yq -P > ./manual-install/containers.yml cd manual-install || exit sed -i "s|'||g" containers.yml -sed -i 's|production:|services:|' containers.yml -sed -i 's|- identifier:| container_name:|' containers.yml -sed -i 's|restartPolicy:|restart:|' containers.yml -sed -i 's|environmentVariables:|environment:|' containers.yml -sed -i '/displayName:/d' containers.yml -sed -i 's|maxShutdownTime:|stop_grace_period:|' containers.yml +sed -i '/display_name:/d' containers.yml +sed -i '/THIS_IS_AIO/d' containers.yml +sed -i "s|%COLLABORA_SECCOMP_POLICY% ||g" containers.yml sed -i '/stop_grace_period:/s/$/s/' containers.yml -sed -i 's|containerName:|image:|' containers.yml sed -i '/: \[\]/d' containers.yml -sed -i 's|dependsOn:|depends_on:|' containers.yml -sed -i 's|- name: |- |' containers.yml +sed -i 's|- source: |- |' containers.yml +sed -i 's|- ip_binding: |- |' containers.yml +sed -i '/AIO_TOKEN/d' containers.yml +sed -i '/AIO_URL/d' containers.yml +sed -i '/DOCKER_SOCKET_PROXY_ENABLED/d' containers.yml +sed -i '/ADDITIONAL_TRUSTED_PROXY/d' containers.yml +sed -i '/TURN_DOMAIN/d' containers.yml +sed -i '/NC_AIO_VERSION/d' containers.yml TCP="$(grep -oP '[%A-Z0-9_]+/tcp' containers.yml | sort -u)" mapfile -t TCP <<< "$TCP" for port in "${TCP[@]}" do solve_port="${port%%/tcp}" - sed -i "s|$port|$solve_port:$solve_port/tcp|" containers.yml + sed -i "s|$solve_port/tcp|$solve_port:$solve_port/tcp|" containers.yml done UDP="$(grep -oP '[%A-Z0-9_]+/udp' containers.yml | sort -u)" @@ -42,7 +62,7 @@ mapfile -t UDP <<< "$UDP" for port in "${UDP[@]}" do solve_port="${port%%/udp}" - sed -i "s|$port|$solve_port:$solve_port/udp|" containers.yml + sed -i "s|$solve_port/udp|$solve_port:$solve_port/udp|" containers.yml done rm -f sample.conf @@ -56,24 +76,44 @@ do sed -i "s|$variable|\${$sole_variable}|g" containers.yml done -sed -i 's|_ENABLED=|_ENABLED=no # Setting this to "yes" enables the option in Nextcloud automatically.|' sample.conf -sed -i 's|TALK_ENABLED=no|TALK_ENABLED=yes|' sample.conf -sed -i 's|COLLABORA_ENABLED=no|COLLABORA_ENABLED=yes|' sample.conf -sed -i 's|COLLABORA_DICTIONARIES=|COLLABORA_DICTIONARIES=de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru # You can change this in order to enable other dictionaries for collabora|' sample.conf +sed -i 's|_ENABLED=|_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.|' sample.conf +sed -i 's|CLAMAV_ENABLED=no.*|CLAMAV_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.|' sample.conf +sed -i 's|TALK_ENABLED=no|TALK_ENABLED="yes"|' sample.conf +sed -i 's|COLLABORA_ENABLED=no|COLLABORA_ENABLED="yes"|' sample.conf +sed -i 's|COLLABORA_DICTIONARIES=|COLLABORA_DICTIONARIES="de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru" # You can change this in order to enable other dictionaries for collabora|' sample.conf sed -i 's|NEXTCLOUD_DATADIR=|NEXTCLOUD_DATADIR=nextcloud_aio_nextcloud_data # You can change this to e.g. "/mnt/ncdata" to map it to a location on your host. It needs to be adjusted before the first startup and never afterwards!|' sample.conf sed -i 's|NEXTCLOUD_MOUNT=|NEXTCLOUD_MOUNT=/mnt/ # This allows the Nextcloud container to access directories on the host. It must never be equal to the value of NEXTCLOUD_DATADIR!|' sample.conf -sed -i 's|NEXTCLOUD_UPLOAD_LIMIT=|NEXTCLOUD_UPLOAD_LIMIT=10G # This allows to change the upload limit of the Nextcloud container|' sample.conf +sed -i 's|NEXTCLOUD_UPLOAD_LIMIT=|NEXTCLOUD_UPLOAD_LIMIT=16G # This allows to change the upload limit of the Nextcloud container|' sample.conf +sed -i 's|NEXTCLOUD_MEMORY_LIMIT=|NEXTCLOUD_MEMORY_LIMIT=512M # This allows to change the PHP memory limit of the Nextcloud container|' sample.conf +sed -i 's|APACHE_MAX_SIZE=|APACHE_MAX_SIZE=17179869184 # This needs to be an integer and in sync with NEXTCLOUD_UPLOAD_LIMIT|' sample.conf sed -i 's|NEXTCLOUD_MAX_TIME=|NEXTCLOUD_MAX_TIME=3600 # This allows to change the upload time limit of the Nextcloud container|' sample.conf -sed -i 's|UPDATE_NEXTCLOUD_APPS=|UPDATE_NEXTCLOUD_APPS=no # When setting to yes, it will automatically update all installed Nextcloud apps upon container startup on saturdays.|' sample.conf -sed -i 's|APACHE_PORT=|APACHE_PORT=443 # Changing this to a different value than 443 will allow you to run it behind a reverse proxy.|' sample.conf -sed -i 's|TALK_PORT=|TALK_PORT=3478 # This allows to adjust the port that the talk container is using.|' sample.conf -sed -i 's|AIO_TOKEN=|AIO_TOKEN=123456 # Has no function but needs to be set!|' sample.conf -sed -i 's|AIO_URL=|AIO_URL=localhost # Has no function but needs to be set!|' sample.conf +sed -i 's|NEXTCLOUD_TRUSTED_CACERTS_DIR=|NEXTCLOUD_TRUSTED_CACERTS_DIR=/usr/local/share/ca-certificates/my-custom-ca # Nextcloud container will trust all the Certification Authorities, whose certificates are included in the given directory.|' sample.conf +sed -i 's|UPDATE_NEXTCLOUD_APPS=|UPDATE_NEXTCLOUD_APPS="no" # When setting to "yes" (with quotes), it will automatically update all installed Nextcloud apps upon container startup on saturdays.|' sample.conf +sed -i 's|APACHE_PORT=|APACHE_PORT=443 # Changing this to a different value than 443 will allow you to run it behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else).|' sample.conf +sed -i 's|APACHE_IP_BINDING=|APACHE_IP_BINDING=0.0.0.0 # This can be changed to e.g. 127.0.0.1 if you want to run AIO behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) and if that is running on the same host and using localhost to connect|' sample.conf +sed -i 's|TALK_PORT=|TALK_PORT=3478 # This allows to adjust the port that the talk container is using. It should be set to something higher than 1024! Otherwise it might not work!|' sample.conf sed -i 's|NC_DOMAIN=|NC_DOMAIN=yourdomain.com # TODO! Needs to be changed to the domain that you want to use for Nextcloud.|' sample.conf sed -i 's|NEXTCLOUD_PASSWORD=|NEXTCLOUD_PASSWORD= # TODO! This is the password of the initially created Nextcloud admin with username "admin".|' sample.conf sed -i 's|TIMEZONE=|TIMEZONE=Europe/Berlin # TODO! This is the timezone that your containers will use.|' sample.conf +sed -i 's|COLLABORA_SECCOMP_POLICY=|COLLABORA_SECCOMP_POLICY=--o:security.seccomp=true # Changing the value to false allows to disable the seccomp feature of the Collabora container.|' sample.conf +sed -i 's|FULLTEXTSEARCH_JAVA_OPTIONS=|FULLTEXTSEARCH_JAVA_OPTIONS="-Xms512M -Xmx512M" # Allows to adjust the fulltextsearch java options.|' sample.conf +sed -i 's|NEXTCLOUD_STARTUP_APPS=|NEXTCLOUD_STARTUP_APPS="deck twofactor_totp tasks calendar contacts notes" # Allows to modify the Nextcloud apps that are installed on starting AIO the first time|' sample.conf +sed -i 's|NEXTCLOUD_ADDITIONAL_APKS=|NEXTCLOUD_ADDITIONAL_APKS=imagemagick # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value.|' sample.conf +sed -i 's|NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS=|NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS=imagick # This allows to add additional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value.|' sample.conf +sed -i 's|INSTALL_LATEST_MAJOR=|INSTALL_LATEST_MAJOR=no # Setting this to yes will install the latest Major Nextcloud version upon the first installation|' sample.conf +sed -i 's|REMOVE_DISABLED_APPS=|REMOVE_DISABLED_APPS=yes # Setting this to no keep Nextcloud apps that are disabled via their switch and not uninstall them if they should be installed in Nextcloud.|' sample.conf sed -i 's|=$|= # TODO! This needs to be a unique and good password!|' sample.conf +grep '# TODO!' sample.conf > todo.conf +grep -v '# TODO!\|_ENABLED' sample.conf > temp.conf +grep '_ENABLED' sample.conf > enabled.conf +cat todo.conf > sample.conf +# shellcheck disable=SC2129 +echo '' >> sample.conf +cat enabled.conf >> sample.conf +echo '' >> sample.conf +cat temp.conf >> sample.conf +rm todo.conf temp.conf enabled.conf cat sample.conf OUTPUT="$(cat containers.yml)" @@ -81,18 +121,23 @@ NAMES="$(grep -oP "container_name:.*" containers.yml | grep -oP 'nextcloud-aio.* mapfile -t NAMES <<< "$NAMES" for name in "${NAMES[@]}" do - OUTPUT="$(echo "$OUTPUT" | sed "/container_name.*$name/i\ \ $name:")" + OUTPUT="$(echo "$OUTPUT" | sed "/container_name.*$name$/i\ \ $name:")" if [ "$name" != "nextcloud-aio-apache" ]; then - OUTPUT="$(echo "$OUTPUT" | sed "/ $name:/i\ ")" + OUTPUT="$(echo "$OUTPUT" | sed "/^ $name:/i\ ")" fi done -OUTPUT="$(echo "$OUTPUT" | sed "/restart: /a\ \ \ \ networks:\n\ \ \ \ \ \ - nextcloud-aio")" +echo "$OUTPUT" > containers.yml -echo 'version: "3.8"' > containers.yml -echo "" >> containers.yml +sed -i '/container_name/d' containers.yml +sed -i 's|^ $||' containers.yml -echo "$OUTPUT" >> containers.yml +# Additional config for collabora +cat << EOL > /tmp/additional-collabora.config + command: \${ADDITIONAL_COLLABORA_OPTIONS} +EOL +sed -i "/^ nextcloud-aio-collabora:/r /tmp/additional-collabora.config" containers.yml +sed -i "/^COLLABORA_DICTIONARIES.*/i ADDITIONAL_COLLABORA_OPTIONS=['--o:security.seccomp=true'] # You can add additional collabora options here by using the array syntax." sample.conf VOLUMES="$(grep -oP 'nextcloud_aio_[a-z_]+' containers.yml | sort -u)" mapfile -t VOLUMES <<< "$VOLUMES" @@ -109,20 +154,12 @@ done cat << NETWORK >> containers.yml networks: - nextcloud-aio: + default: + driver: bridge NETWORK -cat containers.yml > latest.yml -sed -i '/image:/s/$/:latest/' latest.yml +mv containers.yml latest.yml +sed -i "/image:/s/$/:latest/" latest.yml +sed -i 's/\( *- \(\w*\)\)=\${\2\}/\1/' latest.yml -cat containers.yml > latest-arm64.yml -sed -i '/image:/s/$/:latest-arm64/' latest-arm64.yml -sed -i '/ nextcloud-aio-clamav:/,/^ $/d' latest-arm64.yml -sed -i '/nextcloud[-_]aio[-_]clamav/d' latest-arm64.yml -sed -i '/CLAMAV_ENABLED/d' latest-arm64.yml -sed -i '/ nextcloud-aio-onlyoffice:/,/^ $/d' latest-arm64.yml -sed -i '/nextcloud[-_]aio[-_]onlyoffice/d' latest-arm64.yml -sed -i '/ONLYOFFICE_ENABLED/d' latest-arm64.yml -sed -i '/ONLYOFFICE_SECRET/d' latest-arm64.yml - -rm containers.yml +set +ex diff --git a/manual-upgrade.md b/manual-upgrade.md index cf52eb8c..fbc07d3e 100644 --- a/manual-upgrade.md +++ b/manual-upgrade.md @@ -1,25 +1,122 @@ # Manual upgrade -If you do not install any upgrade for around 6-12 months or longer, it can happen that your instance is so outdated that in the meantime the PHP version of the Nextcloud container got bumped to a version that is not compatible with your currently installed Nextcloud version which means that after doing an upgrade after this long time, Nextcloud will suddenly not work anymore. There is unfortunately no way to fix this from the maintainer side if you refrain from upgrading for so long. +If you do not update Nextcloud AIO for a long time (6+ months), when you eventually update in the AIO interface you will find Nextcloud no longer works. This is due to incompatible PHP versions within the nextcloud container. +There is unfortunately no way to fix this from a maintainer POV if you refrain from upgrading for so long. -The only way to fix this on your side is upgrading regularly (e.g. by enabling daily backups which will also automatically upgrade all containers) and following the steps below: +The only way to fix this on your side is upgrading regularly (e.g. by enabling daily backups which will also automatically upgrade all containers) and following the steps below to get back to a normal state: -1. Start all containers from the aio interface (now, it will report that Nextcloud is restarting because it is not able to start due to the above mentioned problem) -1. Do **not** click on `Stop containers` because you will need them running going forward, see below -1. Stop the Nextcloud container and the Apache container by running `sudo docker stop nextcloud-aio-nextcloud && sudo docker stop nextcloud-aio-apache`. -1. Find out with which PHP version your installed Nextcloud is compatible by running `sudo cat /var/lib/docker/volumes/nextcloud_aio_nextcloud/_data/lib/versioncheck.php`. (There you will find information about the max. supported PHP version.) -1. Run the following commands in order to reverse engineer the Nextcloud container: +--- + +## Method 1 using `assaflavie/runlike` + +> [!Warning] +> Please note that this method is apparently currently broken. See https://help.nextcloud.com/t/manual-upgrade-keeps-failing/217164/10 +> So please refer to method 2 using Portainer. + +1. Start all containers from the AIO interface + - Now, it will report that Nextcloud is restarting because it is not able to start due to the above mentioned problem + - #### Do **not** click on `Stop containers` because you will need them running going forward, see below +2. Find out with which PHP version your installed Nextcloud is compatible by running `sudo docker exec nextcloud-aio-nextcloud cat lib/versioncheck.php`. + - There you will find information about the max. supported PHP version + - **Make a mental note of this** +3. Stop the Nextcloud container and the Apache container by running + ```bash + sudo docker stop nextcloud-aio-nextcloud && sudo docker stop nextcloud-aio-apache + ``` +4. Run the following commands in order to reverse engineer the Nextcloud container: ```bash sudo docker pull assaflavie/runlike - echo '#/bin/bash' > /tmp/nextcloud-aio-nextcloud + echo '#!/bin/bash' > /tmp/nextcloud-aio-nextcloud sudo docker run --rm -v /var/run/docker.sock:/var/run/docker.sock assaflavie/runlike -p nextcloud-aio-nextcloud >> /tmp/nextcloud-aio-nextcloud sudo chown root:root /tmp/nextcloud-aio-nextcloud ``` -1. Now open the file with e.g. nano: `sudo nano /tmp/nextcloud-aio-nextcloud` and change the line that should probably be `nextcloud/aio-nextcloud:latest` on x64 or `nextcloud/aio-nextcloud:latest-arm64` on arm64 to the highest compatible PHP version: E.g. `nextcloud/aio-nextcloud:php8.0-latest` on x64 or `nextcloud/aio-nextcloud:php8.0-latest-arm64` on arm64. Then save the file and close it with `[Ctrl]+[o]` -> `[Enter]` and `[Ctrl]+[x]`. -1. After doing so, remove the Nextcloud container with `sudo docker rm nextcloud-aio-nextcloud`. -1. Now start the Nextcloud container with the new tag by simply running `sudo bash /tmp/nextcloud-aio-nextcloud` which at startup should automatically upgrade Nextcloud to a more recent version. If not, make sure that there is no `skip.update` file in the Nextcloud datadir. If there is such a file, simply delete the file and restart the container again.
+5. Now open `/tmp/nextcloud-aio-nextcloud` with a text editor, and edit the container tag: + + +| To change | Replace with | +|----------------------------------------|-----------------------------------------------------| +| `ghcr.io/nextcloud-releases/aio-nextcloud:latest` | `ghcr.io/nextcloud-releases/aio-nextcloud:php{version}-latest` | +| `ghcr.io/nextcloud-releases/aio-nextcloud:latest-arm64` | `ghcr.io/nextcloud-releases/aio-nextcloud:php{version}-latest-arm64` | + + + + - e.g. `ghcr.io/nextcloud-releases/aio-nextcloud:php8.0-latest` or `ghcr.io/nextcloud-releases/aio-nextcloud:php8.0-latest-arm64` + - However, if you are unsure check the ghcr.io (https://github.com/nextcloud-releases/all-in-one/pkgs/container/aio-nextcloud/versions?filters%5Bversion_type%5D=tagged) and docker hub: https://hub.docker.com/r/nextcloud/aio-nextcloud/tags?name=php + - Using nano and the arrow keys to navigate: + - `sudo nano /tmp/nextcloud-aio-nextcloud` making changes as above, then `[Ctrl]+[o]` -> `[Enter]` and `[Ctrl]+[x]` to save and exit. +6. Next, stop and remove the current container: + ```bash + sudo docker stop nextcloud-aio-nextcloud + sudo docker rm nextcloud-aio-nextcloud + ``` +7. Now start the Nextcloud container with the new tag by simply running `sudo bash /tmp/nextcloud-aio-nextcloud` which at startup should automatically upgrade Nextcloud to a more recent version. If not, make sure that there is no `skip.update` file in the Nextcloud datadir. If there is such a file, simply delete the file and restart the container again.
**Info**: You can open the Nextcloud container logs with `sudo docker logs -f nextcloud-aio-nextcloud`. -1. After the Nextcloud container is started (you can tell by looking at the logs), simply restart the container again with `sudo docker restart nextcloud-aio-nextcloud` until it does not install a new Nextcloud update anymore upon the container startup. -1. Now, you should be able to use the AIO interface again by simply stopping the AIO containers and starting them again which should finally bring up your instance again. -1. If not and if you get the same error again, you may repeat the process starting from the beginning again until your Nextcloud version is finally up-to-date. -1. Now, if everything is finally running as usual again, it is recommended to create a backup in order to save the current state. Also you should think about enabling daily backups if doing regularl upgrades is too much effort for you. +8. After the Nextcloud container is started (you can tell by looking at the logs), simply restart the container again with `sudo docker restart nextcloud-aio-nextcloud` until it does not install a new Nextcloud update anymore upon the container startup. +9. Now, you should be able to use the AIO interface again by simply stopping the AIO containers and starting them again which should finally bring up your instance again. +10. If not and if you get the same error again, you may repeat the process starting from the beginning again until your Nextcloud version is finally up-to-date. +11. Now, if everything is finally running as usual again, it is recommended to create a backup in order to save the current state. Consider enabling daily backups if doing regular upgrades is a hassle for you. + +--- + +## Method 2 using Portainer +#### *Approach using portainer if method 1 does not work for you* + +Prerequisite: have all containers from AIO interface running. + +##### 1. Install portainer if not installed: +```bash +docker volume create portainer_data +docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest +``` +- If you have a reverse proxy + - you can setup and navigate using a domain name. +- For the **standard** AIO install + - Open port 9443 on your firewall + - navigate to `https://:9443` +- Accept the insecure self-signed certificate and set an admin password +- If prompted to add an environment + - add local + +##### 2. Within the local portainer environment navigate to the **containers** tab +- Here you should see all the various containers running + +##### 3. Now we need to stop the `nextcloud-aio-nextcloud` and `nextcloud-aio-apache` containers + +- This can be done by selecting the checkbox's next to the containers' name and clicking the **Stop** button at the top + - or you can click into individual containers and stop them there + +##### 4. Find the version of PHP compatible with the running nextcloud container +- navigate to ```nextcloud-aio-nextcloud``` and click on ```logs```, you should see something along the lines of: +```logs +This version of nextcloud is not compatible with >=php 8.2, you are currently running php 8.2.18 +``` +Make **note** of the version which is compatible, rounding down to 1 digit after the dot. + - In this example we would want php 8.1 since anything with 8.2 or above is incompatible + +##### 5. Find the correct container version +In general it should be ```ghcr.io/nextcloud-releases/aio-nextcloud:php8.x-latest-arm64``` or `ghcr.io/nextcloud-releases/aio-nextcloud:php8.x-latest` replacing `x` with the version you require. +However, if you are unsure check the ghcr.io (https://github.com/nextcloud-releases/all-in-one/pkgs/container/aio-nextcloud/versions?filters%5Bversion_type%5D=tagged) and docker hub: https://hub.docker.com/r/nextcloud/aio-nextcloud/tags?name=php + +##### 6. Replace the container +- Navigate to the ```nextcloud-aio-nextcloud``` container within portainer +- Click ```Duplicate/Edit``` +- Within image, change this to the correct version from Step 5 +- Click ```Deploy the container``` + - if you are prompted to force repull the image click the slider and press pull image + +*Navigate to the nextcloud-aio-nextcloud logs and you will see the container updating* + +Once you see no more activities in the logs or a message like ```NOTICE: ready to handle connections```, we've done it! + +#### Now you can handle everything through the AIO interface and stop and restart the containers normally. + +--- + +##### 7. Last Step is removing portainer if you don't want to keep it + +```bash +docker stop portainer +docker rm portainer +docker volume rm portainer_data +``` +- Make sure you close port 9443 on your firewall and delete any necessary reverse proxy hosts. diff --git a/migration.md b/migration.md index 7649be6a..5e9b8b25 100644 --- a/migration.md +++ b/migration.md @@ -1,30 +1,31 @@ # How to migrate from an already existing Nextcloud installation to Nextcloud AIO? -There are basically three ways how to migrate from an already existing Nextcloud installation to Nextcloud AIO: +There are basically three ways how to migrate from an already existing Nextcloud installation to Nextcloud AIO (if you ran AIO on the former installation already, you can follow [these steps](https://github.com/nextcloud/all-in-one#how-to-migrate-from-aio-to-aio)): -1. Migrate only the files which is the easiest way -1. Migrate the files and the database which is much more complicated +1. Migrate only the files which is the easiest way (this excludes all calendar data for example) +1. Migrate the files and the database which is much more complicated (and doesn't work on former snap installations) 1. Use the user_migration app that allows to migrate some of the user's data from a former instance to a new instance but needs to be done manually for each user ## Migrate only the files -**Please note**: If you used groupfolders or encrypted your files before, you will need to restore the database, as well! +**Please note**: If you used groupfolders or encrypted your files before, you will need to restore the database, as well! (This will also exclude all calendar data for example). The procedure for migrating only the files works like this: -1. Take a backup of your former instance (especially from your datadirectory) +1. Take a backup of your former instance (especially from your datadirectory, see `'datadirectory'` in your `config.php`) 1. Install Nextcloud AIO on a new server/linux installation, enter your domain and wait until all containers are running 1. Recreate all users that were present on your former installation 1. Take a backup using Nextcloud AIO's built-in backup solution (so that you can easily restore to this state again) (Note: this will stop all containers and is expected: don't start the container again at this point!) -1. Restore the datadirectory of your former instance into the following directory: `/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/` -1. Next, run `sudo chown -R 33:0 /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/*` and `sudo chmod -R 750 /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/*` to apply the correct permissions +1. Restore the datadirectory of your former instance: for `/path/to/old/nextcloud/data/` run `sudo docker cp --follow-link /path/to/old/nextcloud/data/. nextcloud-aio-nextcloud:/mnt/ncdata/` Note: the `/.` and `/` at the end are necessary. +1. Next, run `sudo docker run --rm --volume nextcloud_aio_nextcloud_data:/mnt/ncdata:rw alpine chown -R 33:0 /mnt/ncdata/` and `sudo docker run --rm --volume nextcloud_aio_nextcloud_data:/mnt/ncdata:rw alpine chmod -R 750 /mnt/ncdata/` to apply the correct permissions. (Or if `NEXTCLOUD_DATADIR` was provided, apply `chown -R 33:0` and `chmod -R 750` to the chosen path.) 1. Start the containers again and wait until all containers are running -1. Run `sudo docker exec -it nextcloud-aio-nextcloud php occ files:scan-app-data && sudo docker exec -it nextcloud-aio-nextcloud php occ files:scan --all` in order to scan all files in the datadirectory. +1. Run `sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ files:scan-app-data && sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ files:scan --all` in order to scan all files in the datadirectory. +1. If the restored data is older than any clients you want to continue to sync, for example if the server was down for a period of time during migration, you may want to take a look at [Synchronising with clients after migration](/migration.md#synchronising-with-clients-after-migration) below. ## Migrate the files and the database -**Please note**: this is much more complicated than migrating only the files and also not as failproof so be warned! +**Please note**: this is much more complicated than migrating only the files and also not as failproof so be warned! Also, this will not work on former snap installations as the snap is read-only and thus you cannot install the necessary `pdo_pgsql` PHP extension. So if migrating from snap, you will need to use one of the other methods. However you could try to ask if the snaps maintainer could add this one small PHP extension to the snap here: https://github.com/nextcloud-snap/nextcloud-snap/issues which would allow for an easy migration. The procedure for migrating the files and the database works like this: 1. Make sure that your old instance is on exactly the same version like the version used in Nextcloud AIO. (e.g. 23.0.0) You can find the used version here: [click here](https://github.com/nextcloud/all-in-one/search?l=Dockerfile&q=NEXTCLOUD_VERSION&type=). If not, simply upgrade your former installation to that version or wait until the version used in Nextcloud AIO got updated to the same version of your former installation or the other way around. -1. Take a backup of your former instance (especially from your datadirectory and database) +1. First, on the old instance, update all Nextcloud apps to its latest version via the app management site (important for the restore later on). Then take a backup of your former instance (especially from your datadirectory and database). 1. If your former installation didn't use Postgresql already, you will now need to convert your old installation to use Postgresql as database temporarily (in order to be able to perform a pg_dump afterwards): 1. Install Postgresql on your former installation: on a Debian based OS should the following command work: ``` @@ -32,20 +33,22 @@ The procedure for migrating the files and the database works like this: ``` 1. Create a new database by running: ``` - export PG_USER="ncadmin" + export PG_USER="ncadmin" # This is a temporary user that gets created for the dump but is then overwritten by the correct one later on export PG_PASSWORD="my-temporary-password" export PG_DATABASE="nextcloud_db" sudo -u postgres psql < - Further information on the conversion is additionally available here: https://docs.nextcloud.com/server/stable/admin_manual/configuration_database/db_conversion.html#converting-database-type + **Please note:** You might need to change the ip-address `127.0.0.1` and adjust the occ command (`occ`) based on your exact installation. Further information on the conversion is additionally available here: https://docs.nextcloud.com/server/stable/admin_manual/configuration_database/db_conversion.html#converting-database-type
+ **Troubleshooting:** If you get an error that it could not find a driver for the conversion, you most likely need to install the PHP extension `pdo_pgsql`. 1. Hopefully does the conversion finish successfully. If not, simply restore your old Nextcloud installation from backup. If yes, you should now log in to your Nextcloud and test if everything works and if all data has been converted successfully. 1. If everything works as expected, feel free to continue with the steps below. 1. Now, run a pg_dump to get an export of your current database. Something like the following command should work: @@ -54,9 +57,9 @@ The procedure for migrating the files and the database works like this: ``` **Please note:** The exact name of the database export file is important! (`database-dump.sql`)
And of course you need to to use the correct name that the Postgresql database has for the export (if `$PG_DATABASE` doesn't work directly). -1. At this point, you can finally install Nextcloud AIO on a new server/linux installation, enter your domain in the AIO interface (use the same domain that you used on your former installation) and wait until all containers are running. Then you should check the included Nextcloud version by running `sudo docker inspect nextcloud-aio-nextcloud | grep NEXTCLOUD_VERSION`. -1. Next, take a backup using Nextcloud AIO's built-in backup solution (so that you can easily restore to this state again) (Note: this will stop all containers and is expected: don't start the container again at this point!) -1. Now, we are slowly starting to import your files and database. First, you need to modify the datadirectory that is stored inside the database export: +1. At this point, you can finally install Nextcloud AIO on a new server/linux installation, enter your domain in the AIO interface (use the same domain that you used on your former installation) and wait until all containers are running. Then you should check the included Nextcloud version by running `sudo docker inspect nextcloud-aio-nextcloud | grep NEXTCLOUD_VERSION`. On the AIO interface, use the passphrase to connect to your newly created Nextcloud instance's admin account. There, install all the Nextcloud apps that were installed on the old Nextcloud installation. If you don't, the migration will show them as installed, but they won't work. +1. Next, take a backup using Nextcloud AIO's built-in backup solution (so that you can easily restore to this state again). Once finished, all containers are automatically stopped and is expected: **don't start the container again at this point!** +1. Now, with the containers still stopped, we are slowly starting to import your files and database. First, you need to modify the datadirectory that is stored inside the database export: 1. Find out what the directory of your old Nextcloud installation is by e.g. opening the config.php file and looking at the value `datadirectory`. 1. Now, create a copy of the database file so that you can simply restore it if you should make a mistake while editing: `cp database-dump.sql database-dump.sql.backup` 1. Next, open the database export with e.g. nano: `nano database-dump.sql` @@ -64,22 +67,39 @@ The procedure for migrating the files and the database works like this: 1. Type in `local::/your/old/datadir/` which should bring up the exact line where you need to modify the path to use the one used in Nextcloud AIO, instead. 1. Change it to look like this: `local::/mnt/ncdata/`. 1. Now save the file by pressing `[CTRL] + [o]` then `[ENTER]` and close nano by pressing `[CTRL] + [x]` - 1. In order to make sure that everything is good, you can now run `grep "/your/old/datadir" database-dump.sql` which should not bring up further results. + 1. In order to make sure that everything is good, you can now run `grep "/your/old/datadir" database-dump.sql` which should not bring up further results.
+ 1. **Please note:** Unfortunately it is not possible to import a database dump from a former database owner with the name `nextcloud`. You can check if that is the case with this command: `grep "Name: oc_appconfig; Type: TABLE; Schema: public; Owner:" database-dump.sql | grep -oP 'Owner:.*$' | sed 's|Owner:||;s| ||g'`. If it returns `nextcloud`, you need to rename the owner in the dump file manually. A command like the following should work, however please note that it is possible that it will overwrite wrong lines. You can thus first check which lines it will change with `grep "Owner: nextcloud$" database-dump.sql`. If only correct looking lines get returned, feel free to change them with `sed -i 's|Owner: nextcloud$|Owner: ncadmin|' database-dump.sql`. +The same applies for the second statement, check with `grep " OWNER TO nextcloud;$" database-dump.sql` and replace with `sed -i 's| OWNER TO nextcloud;$| OWNER TO ncadmin;|' database-dump.sql`. 1. Next, copy the database dump into the correct place and prepare the database container which will import from the database dump automatically the next container start: ``` - sudo rm /var/lib/docker/volumes/nextcloud_aio_database_dump/_data/database-dump.sql - sudo cp database-dump.sql /var/lib/docker/volumes/nextcloud_aio_database_dump/_data/ - sudo chmod 777 /var/lib/docker/volumes/nextcloud_aio_database_dump/_data/database-dump.sql - sudo rm /var/lib/docker/volumes/nextcloud_aio_database_dump/_data/initial-cleanup-done + sudo docker run --rm --volume nextcloud_aio_database_dump:/mnt/data:rw alpine rm /mnt/data/database-dump.sql + sudo docker cp database-dump.sql nextcloud-aio-database:/mnt/data/ + sudo docker run --rm --volume nextcloud_aio_database_dump:/mnt/data:rw alpine chmod 777 /mnt/data/database-dump.sql + sudo docker run --rm --volume nextcloud_aio_database_dump:/mnt/data:rw alpine rm /mnt/data/initial-cleanup-done ``` -1. If the commands above were executed successfully, restore the datadirectory of your former instance into the following directory: `/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/` -1. Next, run `sudo chown -R 33:0 /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/*` and `sudo chmod -R 750 /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/*`to apply the correct permissions -1. Edit the Nextcloud AIO config.php file that is stored in `/var/lib/docker/volumes/nextcloud_aio_nextcloud/_data/config/config.php` and modify only `passwordsalt`, `secret`, `instanceid` and set it to the old values that you used on your old installation. If you are brave, feel free to modify further values e.g. add your old LDAP config or S3 storage config. (Some things like Mail server config can be added back using Nextcloud's webinterface later on). +1. If the commands above were executed successfully, restore the datadirectory of your former instance into your datadirectory: `sudo docker run --rm --volume nextcloud_aio_nextcloud_data:/mnt/ncdata:rw alpine sh -c "rm -rf /mnt/ncdata/*"` and `sudo docker cp --follow-link /path/to/nextcloud/data/. nextcloud-aio-nextcloud:/mnt/ncdata/` Note: the `/.` and `/` at the end are necessary. (Or if `NEXTCLOUD_DATADIR` was provided, first delete the files in there and then copy the files to the chosen path.) +1. Next, run `sudo docker run --rm --volume nextcloud_aio_nextcloud_data:/mnt/ncdata:rw alpine chown -R 33:0 /mnt/ncdata/` and `sudo docker run --rm --volume nextcloud_aio_nextcloud_data:/mnt/ncdata:rw alpine chmod -R 750 /mnt/ncdata/` to apply the correct permissions on the datadirectory. (Or if `NEXTCLOUD_DATADIR` was provided, apply `chown -R 33:0` and `chmod -R 750` to the chosen path.) +1. Edit the Nextcloud AIO config.php file using `sudo docker run -it --rm --volume nextcloud_aio_nextcloud:/var/www/html:rw alpine sh -c "apk add --no-cache nano && nano /var/www/html/config/config.php"` and modify only `passwordsalt`, `secret`, `instanceid` and set it to the old values that you used on your old installation. If you are brave, feel free to modify further values e.g. add your old LDAP config or S3 storage config. (Some things like Mail server config can be added back using Nextcloud's webinterface later on). 1. When you are done and saved your changes to the file, finally start the containers again and wait until all containers are running. -1. As last step, install all apps again that were installed before on your old instance by using the webinterface. Now the whole Nextcloud instance should work again.
If not, feel free to restore the AIO instance from backup and start at step 8 again. +If the restored data is older than any clients you want to continue to sync, for example if the server was down for a period of time during migration, you may want to take a look at [Synchronising with clients after migration](/migration.md#synchronising-with-clients-after-migration) below. + ## Use the user_migration app A new way since the Nextcloud update to 24 is to use the new [user_migration app](https://apps.nextcloud.com/apps/user_migration#app-gallery). It allows to export the most important data on one instance and import it on a different Nextcloud instance. For that, you need to install and enable the user_migration app on your old instance, trigger the export for the user, create the user on the new instance, log in with that user and import the archive that was created during the export. This then needs to be done for each user that you want to migrate. + +If the restored data is older than any clients you want to continue to sync, for example if the server was down for a period of time during migration, you may want to take a look at [Synchronising with clients after migration](/migration.md#synchronising-with-clients-after-migration) below. + +# Synchronising with clients after migration +#### From https://docs.nextcloud.com/server/latest/admin_manual/maintenance/restore.html#synchronising-with-clients-after-data-recovery +By default the Nextcloud server is considered the authoritative source for the data. If the data on the server and the client differs clients will default to fetching the data from the server. + +If the recovered backup is outdated the state of the clients may be more up to date than the state of the server. In this case also make sure to run `sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ maintenance:data-fingerprint` command afterwards. It changes the logic of the synchronisation algorithm to try an recover as much data as possible. Files missing on the server are therefore recovered from the clients and in case of different content the users will be asked. + +>[!Note] +>The usage of maintenance:data-fingerprint can cause conflict dialogues and difficulties deleting files on the client. Therefore it’s only recommended to prevent dataloss if the backup was outdated. + + +If you are running multiple application servers you will need to make sure the config files are synced between them so that the updated data-fingerprint is applied on all instances. diff --git a/multiple-instances.md b/multiple-instances.md index b9f5f804..00386e1b 100644 --- a/multiple-instances.md +++ b/multiple-instances.md @@ -3,17 +3,224 @@ It is possible to run multiple instances of AIO on one server. There are two ways to achieve this: The normal way is creating multiple VMs, installing AIO in [reverse proxy mode](./reverse-proxy.md) in each of them and having one reverse proxy in front of them that points to each VM (you also need to [use a different `TALK_PORT`](https://github.com/nextcloud/all-in-one#how-to-adjust-the-talk-port) for each of them). The second and more advanced way is creating multiple users on the server and using docker rootless for each of them in order to install multiple instances on the same server. -Below is described more in detail how the the second way works. ## Run multiple AIO instances on the same server with docker rootless 1. Create as many linux users as you need first. The easiest way is to use `sudo adduser` and follow the setup for that. Make sure to create a strong unique password for each of them and write it down! -1. Log in as each of the users e.g. by opening a new SSH connection and install docker rootless for each of them by following step 0-4 of the [docker rootless documentation](./docker-rootless.md). -1. Then install AIO in reverse proxy mode by using the command that is descriebed in step 2 and 3 of the [reverse proxy documentation](./reverse-proxy.md) but use a different `APACHE_PORT` and [`TALK_PORT`](https://github.com/nextcloud/all-in-one#how-to-adjust-the-talk-port) for each instance as otherwise it will bug out. Also make sure to adjust the docker socket and `DOCKER_SOCKET_PATH` correctly for each of them by following step 6 of the [docker rootless documentation](./docker-rootless.md). Additionally, modify `--publish 8080:8080` to a different port for each container, e.g. `8081:8080` as otherwise it will not work.
+1. Log in as each of the users by opening a new SSH connection as the user and install docker rootless for each of them by following step 0-1 and 3-4 of the [docker rootless documentation](./docker-rootless.md) (you can skip step 2 in this case). +1. Then install AIO in reverse proxy mode by using the command that is described in step 2 and 3 of the [reverse proxy documentation](./reverse-proxy.md) but use a different `APACHE_PORT` and [`TALK_PORT`](https://github.com/nextcloud/all-in-one#how-to-adjust-the-talk-port) for each instance as otherwise it will bug out. Also make sure to adjust the docker socket and `WATCHTOWER_DOCKER_SOCKET_PATH` correctly for each of them by following step 6 of the [docker rootless documentation](./docker-rootless.md). Additionally, modify `--publish 8080:8080` to a different port for each container, e.g. `8081:8080` as otherwise it will not work.
**⚠️ Please note:** If you want to adjust the `NEXTCLOUD_DATADIR`, make sure to apply the correct permissions to the chosen path as documented at the bottom of the [docker rootless documentation](./docker-rootless.md). Also for the built-in backup to work, the target path needs to have the correct permissions as documented there, too. 1. Now install your webserver of choice on the host system. It is recommended to use caddy for this as it is by far the easiest solution. You can do so by following https://caddyserver.com/docs/install#debian-ubuntu-raspbian or below. (It needs to be installed directly on the host or on a different server in the same network). 1. Next create your Caddyfile with multiple entries and domains for the different instances like described in step 1 of the [reverse proxy documentation](./reverse-proxy.md). Obviously each domain needs to point correctly to the chosen `APACHE_PORT` that you've configured before. Then start Caddy which should automatically get the needed certificates for you if your domains are configured correctly and ports 80 and 443 are forwarded to your server. 1. Now open each of the AIO interfaces by opening `https://ip.address.of.this.server:8080` or e.g. `https://ip.address.of.this.server:8081` or as chosen during step 3 of this documentation. 1. Finally type in the domain that you've configured for each of the instances during step 5 of this documentation and you are done. -1. Please also do not forget to open each chosen `TALK_PORT` UPD and TCP in your firewall/router as otherwise Talk will not work correctly! +1. Please also do not forget to open/forward each chosen `TALK_PORT` UDP and TCP in your firewall/router as otherwise Talk will not work correctly! Now everything should be set up correctly and you should have created multiple working instances of AIO on the same server! + + +## Run multiple AIO instances on the same server inside their own virtual machines +This guide will walk you through creating and configuring two (or more) Debian-based VMs (with "reverse proxy mode" Nextcloud AIO installed in each VM), behind one Caddy reverse proxy, all running on one host physical machine (like a laptop or desktop PC). It's highly recommend to follow the steps in order. Steps 1 through 4 will need to be repeated. Steps 5 through 8 only need to be completed once. All commands are expected to be run as root. + +
PLEASE READ: A few expectations about your network +This guide assumes that you have forwarded ports 443 and 8443 to your host physical machine via your router's configuration page, and either set up Dynamic DNS or obtained a static outbound IP address from your ISP. If this is not the case, or if you are brand-new to networking, you probably should not proceed with this guide, unless you are just using it for educational purposes. Proper network setup and security is critical when it comes to keeping your data safe. You may consider hosting using a VPS instead, or choosing one of Nextcloud's trusted providers. +
+ +
A note for VPS users +If you want to do this on a VPS, and your VPS is KVM-based and provides a static IP address, you can likely benefit from this guide too! Simply replace the words "host physical machine" with "VPS" and follow along. +
+ +**Before starting:** Make sure your host physical machine has enough resources. A host machine with 8GB RAM and 100GB storage is sufficient for running two fairly minimal VMs, with 2GB RAM and 32GB storage allocated to each VM. This guide assumes you have these resources at the minimum. This is fine for just testing the setup, but you will probably want to allocate more resources to your VMs if you plan to use this for day-to-day use. +If your host machine has more than 8GB memory available, and you plan to enable any of the optional containers (Nextcloud Office, Talk, Imaginary, etc.) in any of your instances, then you should definitely allocate more memory to the VM hosting that instance. In other words, before turning on any extra features inside a particular AIO interface, make sure you've first allocated enough resources to the VM that the instance is running inside. If in doubt, the AIO interface itself gives great recommendations for extra CPU and RAM allocation. + +**Additional prerequisites:** Your host physical machine needs to have virtualization enabled in it's UEFI/BIOS. It also needs a few tools installed in order to create VMs. Assuming your host machine is a bare-bones Ubuntu or Debian Linux server without a desktop environment installed, the easiest way to create VMs is to install *QEMU*, *virsh*, *virt-install*, and a few extra packages to support UEFI booting and network config ([more info](https://wiki.debian.org/KVM)). You only need to do this once. To do this, run this command (**on the host physical machine**): + +```shell +# For host machines running Ubuntu Server or Debian: +apt install --no-install-recommends qemu-system qemu-utils libvirt-clients libvirt-daemon-system virtinst ovmf bridge-utils dnsmasq-base +``` + +**Let's begin!** This guide assumes that you have two domains where you would like to host two individual AIO instances (one instance per domain). Let's call these domains `example1.com` and `example2.com`. Therefore, we'll create two VMs named `example1-com` and `example2-com` (These are the VM names we'll use below in step 1). + +**Once you're ready, follow steps 1-4 below to set up your VMs. You will configure them one at a time.** + +1. Choose a name for your VM. A good choice is to name each VM the same as the domain name that will be used to access it. +2. Choose the distribution you'd like to install within the VM: +
Ubuntu Server 22.04.4 LTS +

Downloading the .ISO image

+ You must first download an .ISO image to your host machine, and then provide virt-install with the path to that image. + +
# Skip this part if you've already downloaded this image
+   curl -o /tmp/ubuntu-22.04.4-live-server-amd64.iso https://releases.ubuntu.com/jammy/ubuntu-22.04.4-live-server-amd64.iso
+   
+ Note: You may choose a different place to store the .ISO file, but it needs to be somewhere accessible by QEMU. "/tmp" and "/home" work well, but choosing a location like "/root" will cause the next command to fail. +

Creating the VM

+ Now create the Ubuntu Server VM (Don't forget to replace [VM_NAME]): +
virt-install \
+   --name [VM_NAME] \
+   --virt-type kvm \
+   --location /tmp/ubuntu-22.04.4-live-server-amd64.iso,kernel=casper/vmlinuz,initrd=casper/initrd \
+   --os-variant ubuntujammy \
+   --disk size=32 \
+   --memory 2048 \
+   --graphics none \
+   --console pty,target_type=serial \
+   --extra-args "console=ttyS0" \
+   --autostart \
+   --boot uefi
+   
+

Using a different version of Ubuntu Server

+ To use a different Ubuntu Server release, visit this page and find the version you want. You will need to adjust the filename and URL for the curl command, and the location and os-variant for the virt-install command, accordingly. +
+
Debian 11 +

Creating the VM

+ Create the Debian VM (Don't forget to replace [VM_NAME]): +
virt-install \
+   --name [VM_NAME] \
+   --virt-type kvm \
+   --location http://deb.debian.org/debian/dists/bullseye/main/installer-amd64/ \
+   --os-variant debian11 \
+   --disk size=32 \
+   --memory 2048 \
+   --graphics none \
+   --console pty,target_type=serial \
+   --extra-args "console=ttyS0" \
+   --autostart \
+   --boot uefi
+   
+
+
Debian 12 +

Creating the VM

+ Create the Debian VM (Don't forget to replace [VM_NAME]): +
# If the os-variant "debian12" is unknown, try "debiantesting" instead
+   virt-install \
+   --name [VM_NAME] \
+   --virt-type kvm \
+   --location http://deb.debian.org/debian/dists/bookworm/main/installer-amd64/ \
+   --os-variant debian12 \
+   --disk size=32 \
+   --memory 2048 \
+   --graphics none \
+   --console pty,target_type=serial \
+   --extra-args "console=ttyS0" \
+   --autostart \
+   --boot uefi
+   
+
+ +3. Navigate through the text-based installer. Most options can remain as default, but here are some tips: +
For the Ubuntu Server installer + When asked about the "type of installation", you can leave the default "Ubuntu Server" without third-party drivers. You can leave the HTTP proxy information blank. In the "Profile Configuration" section, you can set "Your servers name" (hostname) to the same value as the name you gave to your VM (for example, "example1-com"). The installer will only let you create a non-root user. Note down the password you use here! You may skip enabling Ubuntu Pro. You can allow the partitioner to use the entire disk, this only uses the virtual disk that you defined above in step 2. You'll eventually be given the option to install additional software. Although "Nextcloud" is listed here, you almost certainly do not want to select this option, since you are setting up Nextcloud AIO. You'll be asked about installing "SSH server", this is entirely optional (This lets you easily SSH into the VM in the future in case you have to perform any maintenance, but even if you do not install an SSH server, you can still log in using the "virsh console" command). Finally, disregard the "[FAILED] Failed unmounting /cdrom." message, and press return. +
+
For the Debian installer + When asked, you can set the hostname to the same value as the name you gave to your VM (for example, "example1-com"). You can leave the domain name and HTTP proxy information blank. Allow the installer to create both a root and a non-root user. Note down the password(s) you use here! You can allow the partitioner to use the entire disk, this only uses the virtual disk that you defined above in step 2. When tasksel (Software selection) runs and asks if you want to install additional software, use spacebar and your arrow keys to un-check the "Debian desktop environment" and "GNOME" options. The "SSH server" option is entirely optional (This lets you easily SSH into the VM in the future in case you have to perform any maintenance, but even if you do not install an SSH server, you can still log in using the "virsh console" command). Make sure "standard system utilities" is also checked. Hit tab to select "Continue". Finally, disregard the warning about GRUB, allow it to install to your "primary drive" (again, it's only virtual, and this only applies to the VM- this will not affect the boot configuration of your host physical machine) and select "/dev/vda" for the bootable device. +
+4. Configure your new VM: + + After it has finished installing, the VM will have rebooted and presented you with a login prompt. For Debian, just use `root` as the username, and enter the password you chose during the installation process. Ubuntu restricts root account access, so you'll need to first login with your non-root user, and then run `sudo su -` to elevate your privileges. + + We will now run a few commands to install docker and AIO in reverse proxy mode! As with any other commands, carefully read and try your best to understand them before running them. + + **Each time you reach this step and run the `docker run` command below, you'll need to increment the `TALK_PORT` value. For example: 3478, 3479, etc... You may use other values as long as they don't conflict, and make sure they are [greater than 1024](https://github.com/nextcloud/all-in-one/discussions/2517). Be sure to note down the Talk port number you've assigned to this VM/AIO instance. You will need it later if you decide to enable Nextcloud Talk.** + + Run these commands (**on the VM**): + ```shell + apt install -y curl + + curl -fsSL https://get.docker.com | sh + + # Make sure you increment the TALK_PORT value every time you run this! + docker run \ + --init \ + --sig-proxy=false \ + --name nextcloud-aio-mastercontainer \ + --restart always \ + --publish 8080:8080 \ + --env APACHE_PORT=11000 \ + --env APACHE_IP_BINDING=0.0.0.0 \ + --env TALK_PORT=3478 \ + --volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + ghcr.io/nextcloud-releases/all-in-one:latest + ``` + The last command may take a few minutes. When it's finished, you should see a success message, saying "Initial startup of Nextcloud All-in-One complete!". Now exit the console session with `Ctrl + [c]`. This concludes the setup for this particular VM. + + + --- +6. Go ahead and run through steps 1-4 again in order to set up your second VM. When you're finished, proceed down to step 6. *(Note: If you downloaded the Ubuntu .ISO image and no longer need it, you may delete it now.)* +7. Almost done! All that's left is configuring your reverse proxy. To do this, you first need to [install it](https://caddyserver.com/docs/install#debian-ubuntu-raspbian). Run (**on the host physical machine**): + ```shell + apt update -y + apt install -y debian-keyring debian-archive-keyring apt-transport-https curl + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list + apt update -y + apt install -y caddy + ``` + These commands will ensure that your system is up-to-date and install the latest stable version of Caddy via it's official binary source. +8. To configure Caddy, you need to know the IP address assigned to each VM. Run (**on the host physical machine**): + ```shell + virsh net-dhcp-leases default + ``` + This will show you the VMs you set up, and the IP address corresponding to each of them. Note down each IP and corresponding hostname. + Finally, you will configure Caddy using this information. Open the default Caddyfile with a text editor: + ```shell + nano /etc/caddy/Caddyfile + ``` + Replace everything in this file with the following configuration. Don't forget to edit this sample configuration and substitute in your own domain names and IP addresses. `[DOMAIN_NAME_*]` should be a domain name like `example1.com`, and `[IP_ADDRESS_*]` should be a local IPv4 address like `192.168.122.225`. + ```shell + # Virtual machine #1 - "example1-com" + https://[DOMAIN_NAME_1]:8443 { + reverse_proxy https://[IP_ADDRESS_1]:8080 { + transport http { + tls_insecure_skip_verify + } + } + } + https://[DOMAIN_NAME_1]:443 { + reverse_proxy [IP_ADDRESS_1]:11000 + } + + # Virtual machine #2 - "example2-com" + https://[DOMAIN_NAME_2]:8443 { + reverse_proxy https://[IP_ADDRESS_2]:8080 { + transport http { + tls_insecure_skip_verify + } + } + } + https://[DOMAIN_NAME_2]:443 { + reverse_proxy [IP_ADDRESS_2]:11000 + } + + # (Add more configurations here if you set up more than two VMs!) + ``` + After making this change, you'll need to restart Caddy: + ```shell + systemctl restart caddy + ``` +9. That's it! Now, all that's left is to set up your instances through the AIO interface as usual by visiting `https://example1.com:8443` and `https://example2.com:8443` in a browser. Once you're finished going through each setup, you can access your new instances simply through their domain names. You can host as many instances with as many domain names as you want this way, as long as you have enough system resources. Enjoy! + +
A few extra tips for managing this setup +
    +
  • You can easily connect to a VM to perform maintenance using this command (on the host physical machine):
    virsh console --domain [VM_NAME]
  • +
  • If you chose to install an SSH Server, you can SSH in using this command (on the host physical machine):
    ssh [NONROOT_USER]@[IP_ADDRESS] # By default, OpenSSH does not allow logging in as root
  • +
  • If you mess up the configuration of a VM, you may wish to completely delete it and start fresh with a new one. THIS WILL DELETE ALL DATA ASSOCIATED WITH THE VM INCLUDING ANYTHING IN YOUR AIO DATADIR! If you are sure you would like to do this, run (on the host physical machine):
    virsh destroy --domain [VM_NAME] ; virsh undefine --nvram --domain [VM_NAME] && rm -rfi /var/lib/libvirt/images/[VM_NAME].qcow2
  • +
  • Using Nextcloud Talk will require some extra configuration. Back when you set up your VMs, they were (by default) configured with NAT, meaning they are in their own subnet. The VMs must each instead be bridged, so that your router may directly "see" them (as if they were real, physical devices on your network), and each AIO instance inside each VM must be configured with a different Talk port (like 3478, 3479, etc.). You should have already set these port numbers (back when you first configured the VM in step 4 above), but if you still need to set (or want to change) these values, you can remove the mastercontainer and re-run the initial "docker run" command with a modified Talk port like so. Then, the Talk port for EACH instance needs to be forwarded in your router's settings DIRECTLY to the VM hosting the instance (completely bypassing your host physical machine/reverse proxy). And finally, inside an admin-privileged account (such as the default "admin" account) in each instance, you must visit https://[DOMAIN_NAME]/settings/admin/talk then find the STUN/TURN Settings, and from there set the proper values. If this is too complicated, it may be easier to use public STUN/TURN servers, but I have not tested any of this, rather I'm just sharing what I have found so far (more info available here). If you have figured this out or if any of this information is incorrect, please edit this section!
  • +
  • Configuring daily automatic backups is a bit more involved with this setup. But for the occasional manual borg backup, you can connect a physical SSD/HDD via a cheap USB SATA adapter/dock to a free USB port on your host physical machine, and then use these commands to pass the disk through to a VM of your choosing (on the host physical machine and on the VM):
    virsh attach-device --live --domain [VM_NAME] --file [USB_DEVICE_DEFINITION.xml]
    +   virsh console --domain [VM_NAME]
    +   # (Login to the VM with root privileges)
    +   mkdir -p /mnt/[MOUNT_NAME]
    +   mount /dev/disk/by-label/[DISK_NAME] /mnt/[MOUNT_NAME]
  • + To create the XML device definition file, see this short guide. An SSD/HDD is recommended, but nothing is stopping you from using something as simple as a flash drive for testing if you really want. Finally, to actually perform a manual backup, make sure your disk is properly mounted and then simply use the AIO interface to perform the backup. +
  • If you want to shave off around 8-10 seconds of total boot time when you reboot your host physical machine, a simple trick is to lower the GRUB_TIMEOUT from the default five seconds to one second, on both the host physical machine and each of the VMs. You can also remove the delay, but it's generally safer to leave at least one second. (Always be extremely careful when editing GRUB config, especially on the host physical machine, as an incorrect configuration can prevent your device from booting!)
  • +
+
diff --git a/nextcloud-aio-helm-chart/Chart.yaml b/nextcloud-aio-helm-chart/Chart.yaml new file mode 100755 index 00000000..6288a381 --- /dev/null +++ b/nextcloud-aio-helm-chart/Chart.yaml @@ -0,0 +1,13 @@ +name: nextcloud-aio-helm-chart +description: A generated Helm Chart for Nextcloud AIO from Skippbox Kompose +version: 12.5.0 +apiVersion: v2 +keywords: + - latest + - nextcloud + - helm-chart + - open-source + - cloud +sources: + - https://github.com/nextcloud/all-in-one/tree/main/nextcloud-aio-helm-chart +home: https://github.com/nextcloud/all-in-one/tree/main/nextcloud-aio-helm-chart diff --git a/nextcloud-aio-helm-chart/readme.md b/nextcloud-aio-helm-chart/readme.md new file mode 100755 index 00000000..cb31e601 --- /dev/null +++ b/nextcloud-aio-helm-chart/readme.md @@ -0,0 +1,45 @@ +# Nextcloud AIO Helm-chart + +> [!NOTE] +> For an enterprise-ready and scalable deployment method based on Helm Charts (also available for Podman and OpenShift), please [contact Nextcloud GmbH](https://nextcloud.com/enterprise/). + +> [!IMPORTANT] +> This Helm-Chart is not intended to be used with Ingress as it handles TLS itself via the built-in apache container and exposes a Loadbalancer port itself on the Cluster. See the [apache service](https://github.com/nextcloud/all-in-one/blob/main/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-service.yaml). However if the Cluster is used behind NAT, you can adjust `APACHE_PORT` to a different one than 443 and do the TLS offloading on an external Reverse Proxy that forwards the traffic to the configured port via http. If you really need the Ingress feature, please [contact Nextcloud GmbH](https://nextcloud.com/enterprise/) as we offer an enterprise-ready and scalable deployment method based on Helm Charts that also allows Ingress to be used. + +You can run the containers that are build for AIO with Kubernetes using this Helm chart. This comes with a few downsides, that are discussed below. + +### Advantages +- You can run it without a container having access to the docker socket +- You can run the containers with Kubernetes + +### Disadvantages +- You lose the AIO interface +- You lose update notifications and automatic updates +- You lose all AIO backup and restore features +- You lose all community containers: https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers +- **You need to know what you are doing** +- For updating, you need to strictly follow the at the bottom described update routine +- You need to monitor yourself if the volumes have enough free space and increase them if they don't by adjusting their size in values.yaml +- Probably more + +## How to use this? + +First download this file: https://raw.githubusercontent.com/nextcloud/all-in-one/main/nextcloud-aio-helm-chart/values.yaml and adjust at least all values marked with `# TODO!`
+⚠️ **Warning**: Do not use the symbols `@` and `:` in your passwords. These symbols are used to build database connection strings. You will experience issues when using these symbols! + +Then run: + +``` +helm repo add nextcloud-aio https://nextcloud.github.io/all-in-one/ +helm install nextcloud-aio nextcloud-aio/nextcloud-aio-helm-chart -f values.yaml +``` + +And after a while, everything should be set up. + +## How to update? +Since the values of this helm chart may change in the future, it is highly recommended to strictly follow the following procedure whenever you want to upgrade it. +1. Stop all running pods +1. Back up all volumes that got created by the Helm chart and the values.yaml file +1. Run `helm repo update nextcloud-aio` in order to get the updated yaml files from the repository +1. Now download the updated values.yaml file from https://raw.githubusercontent.com/nextcloud/all-in-one/main/nextcloud-aio-helm-chart/values.yaml and compare that with the one that you currently have locally. Look for variables that changed or got added. You can use the diff command to compare them. +1. After the file update was successful, simply run `helm install my-release nextcloud-aio/nextcloud-aio-helm-chart -f values.yaml` to update to the new version. diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-deployment.yaml new file mode 100755 index 00000000..e540791c --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-deployment.yaml @@ -0,0 +1,107 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-apache + name: nextcloud-aio-apache + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-apache + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-apache + spec: + securityContext: + # The items below only work in pod context + fsGroup: 33 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 33 + runAsGroup: 33 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: ADDITIONAL_TRUSTED_DOMAIN + value: "{{ .Values.ADDITIONAL_TRUSTED_DOMAIN }}" + - name: APACHE_HOST + value: nextcloud-aio-apache + - name: APACHE_MAX_SIZE + value: "{{ .Values.APACHE_MAX_SIZE }}" + - name: APACHE_MAX_TIME + value: "{{ .Values.NEXTCLOUD_MAX_TIME }}" + - name: APACHE_PORT + value: "{{ .Values.APACHE_PORT }}" + - name: COLLABORA_HOST + value: nextcloud-aio-collabora + - name: NC_DOMAIN + value: "{{ .Values.NC_DOMAIN }}" + - name: NEXTCLOUD_HOST + value: nextcloud-aio-nextcloud + - name: NOTIFY_PUSH_HOST + value: nextcloud-aio-notify-push + - name: ONLYOFFICE_HOST + value: nextcloud-aio-onlyoffice + - name: TALK_HOST + value: nextcloud-aio-talk + - name: TZ + value: "{{ .Values.TIMEZONE }}" + - name: WHITEBOARD_HOST + value: nextcloud-aio-whiteboard + image: ghcr.io/nextcloud-releases/aio-apache:20260122_105751 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-apache + ports: + - containerPort: {{ .Values.APACHE_PORT }} + protocol: TCP + - containerPort: {{ .Values.APACHE_PORT }} + protocol: UDP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + add: ["NET_BIND_SERVICE"] + volumeMounts: + - mountPath: /var/www/html + name: nextcloud-aio-nextcloud + readOnly: true + - mountPath: /mnt/data + name: nextcloud-aio-apache + volumes: + - name: nextcloud-aio-nextcloud + persistentVolumeClaim: + claimName: nextcloud-aio-nextcloud + - name: nextcloud-aio-apache + persistentVolumeClaim: + claimName: nextcloud-aio-apache diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-persistentvolumeclaim.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-persistentvolumeclaim.yaml new file mode 100755 index 00000000..773d198f --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-apache + name: nextcloud-aio-apache + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.APACHE_STORAGE_SIZE }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-service.yaml new file mode 100755 index 00000000..98e33a4d --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-apache + name: nextcloud-aio-apache + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + type: LoadBalancer + externalTrafficPolicy: Local + ports: + - name: "{{ .Values.APACHE_PORT }}" + port: {{ .Values.APACHE_PORT }} + targetPort: {{ .Values.APACHE_PORT }} + - name: {{ .Values.APACHE_PORT }}-udp + port: {{ .Values.APACHE_PORT }} + protocol: UDP + targetPort: {{ .Values.APACHE_PORT }} + selector: + io.kompose.service: nextcloud-aio-apache diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-deployment.yaml new file mode 100755 index 00000000..57ec7739 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-deployment.yaml @@ -0,0 +1,100 @@ +{{- if eq .Values.CLAMAV_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-clamav + name: nextcloud-aio-clamav + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-clamav + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-clamav + spec: + securityContext: + # The items below only work in pod context + fsGroup: 100 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 100 + runAsGroup: 100 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + initContainers: + - name: init-subpath + image: ghcr.io/nextcloud-releases/aio-alpine:20260122_105751 + command: + - mkdir + - "-p" + - /nextcloud-aio-clamav/data + volumeMounts: + - name: nextcloud-aio-clamav + mountPath: /nextcloud-aio-clamav + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + containers: + - env: + - name: MAX_SIZE + value: "{{ .Values.NEXTCLOUD_UPLOAD_LIMIT }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-clamav:20260122_105751 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-clamav + ports: + - containerPort: 3310 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + volumeMounts: + - mountPath: /var/lib/clamav + subPath: data + name: nextcloud-aio-clamav + volumes: + - name: nextcloud-aio-clamav + persistentVolumeClaim: + claimName: nextcloud-aio-clamav +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-persistentvolumeclaim.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-persistentvolumeclaim.yaml new file mode 100755 index 00000000..ebb19681 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-persistentvolumeclaim.yaml @@ -0,0 +1,18 @@ +{{- if eq .Values.CLAMAV_ENABLED "yes" }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-clamav + name: nextcloud-aio-clamav + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.CLAMAV_STORAGE_SIZE }} +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-service.yaml new file mode 100755 index 00000000..8b236093 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.CLAMAV_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-clamav + name: nextcloud-aio-clamav + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "3310" + port: 3310 + targetPort: 3310 + selector: + io.kompose.service: nextcloud-aio-clamav +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-deployment.yaml new file mode 100755 index 00000000..cd4e1368 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-deployment.yaml @@ -0,0 +1,71 @@ +{{- if eq .Values.COLLABORA_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-collabora + name: nextcloud-aio-collabora + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-collabora + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-collabora + spec: + containers: + - args: {{ .Values.ADDITIONAL_COLLABORA_OPTIONS | default list | toJson }} + env: + - name: DONT_GEN_SSL_CERT + value: "1" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + - name: aliasgroup1 + value: https://{{ .Values.NC_DOMAIN }}:443,http://nextcloud-aio-apache:23973 + - name: dictionaries + value: "{{ .Values.COLLABORA_DICTIONARIES }}" + - name: extra_params + value: --o:ssl.enable=false --o:ssl.termination=true --o:logging.disable_server_audit=true --o:logging.level=warning --o:logging.level_startup=warning --o:welcome.enable=false --o:remote_font_config.url=https://{{ .Values.NC_DOMAIN }}/apps/richdocuments/settings/fonts.json --o:net.post_allow.host[0]=.+ + - name: server_name + value: "{{ .Values.NC_DOMAIN }}" + {{- if contains "--o:support_key=" (join " " (.Values.ADDITIONAL_COLLABORA_OPTIONS | default list)) }} + image: ghcr.io/nextcloud-releases/aio-collabora-online:20260122_105751 + {{- else }} + image: ghcr.io/nextcloud-releases/aio-collabora:20260122_105751 + {{- end }} + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-collabora + ports: + - containerPort: 9980 + protocol: TCP + securityContext: + capabilities: + add: + - MKNOD + - CAP_SYS_ADMIN + - SYS_CHROOT + - FOWNER + - CHOWN +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-service.yaml new file mode 100755 index 00000000..5c81ef3e --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.COLLABORA_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-collabora + name: nextcloud-aio-collabora + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "9980" + port: 9980 + targetPort: 9980 + selector: + io.kompose.service: nextcloud-aio-collabora +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-deployment.yaml new file mode 100755 index 00000000..be6a9c90 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-deployment.yaml @@ -0,0 +1,108 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-database + name: nextcloud-aio-database + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-database + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-database + spec: + securityContext: + # The items below only work in pod context + fsGroup: 999 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 999 + runAsGroup: 999 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + initContainers: + - name: init-subpath + image: ghcr.io/nextcloud-releases/aio-alpine:20260122_105751 + command: + - mkdir + - "-p" + - /nextcloud-aio-database/data + volumeMounts: + - name: nextcloud-aio-database + mountPath: /nextcloud-aio-database + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + containers: + - env: + - name: PGTZ + value: "{{ .Values.TIMEZONE }}" + - name: POSTGRES_DB + value: nextcloud_database + - name: POSTGRES_PASSWORD + value: "{{ .Values.DATABASE_PASSWORD }}" + - name: POSTGRES_USER + value: nextcloud + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-postgresql:20260122_105751 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-database + ports: + - containerPort: 5432 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + volumeMounts: + - mountPath: /var/lib/postgresql/data + subPath: data + name: nextcloud-aio-database + - mountPath: /mnt/data + name: nextcloud-aio-database-dump + terminationGracePeriodSeconds: 1800 + volumes: + - name: nextcloud-aio-database + persistentVolumeClaim: + claimName: nextcloud-aio-database + - name: nextcloud-aio-database-dump + persistentVolumeClaim: + claimName: nextcloud-aio-database-dump diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-dump-persistentvolumeclaim.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-dump-persistentvolumeclaim.yaml new file mode 100755 index 00000000..49135452 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-dump-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-database-dump + name: nextcloud-aio-database-dump + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.DATABASE_DUMP_STORAGE_SIZE }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-persistentvolumeclaim.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-persistentvolumeclaim.yaml new file mode 100755 index 00000000..7b753e22 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-database + name: nextcloud-aio-database + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.DATABASE_STORAGE_SIZE }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-service.yaml new file mode 100755 index 00000000..45fdce3a --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-database + name: nextcloud-aio-database + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "5432" + port: 5432 + targetPort: 5432 + selector: + io.kompose.service: nextcloud-aio-database diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-elasticsearch-persistentvolumeclaim.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-elasticsearch-persistentvolumeclaim.yaml new file mode 100755 index 00000000..44458a8d --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-elasticsearch-persistentvolumeclaim.yaml @@ -0,0 +1,18 @@ +{{- if eq .Values.FULLTEXTSEARCH_ENABLED "yes" }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-elasticsearch + name: nextcloud-aio-elasticsearch + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.ELASTICSEARCH_STORAGE_SIZE }} +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-deployment.yaml new file mode 100755 index 00000000..bed60a0c --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-deployment.yaml @@ -0,0 +1,85 @@ +{{- if eq .Values.FULLTEXTSEARCH_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-fulltextsearch + name: nextcloud-aio-fulltextsearch + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-fulltextsearch + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-fulltextsearch + spec: + initContainers: + - name: init-volumes + image: ghcr.io/nextcloud-releases/aio-alpine:20260122_105751 + command: + - chmod + - "777" + - /nextcloud-aio-elasticsearch + volumeMounts: + - name: nextcloud-aio-elasticsearch + mountPath: /nextcloud-aio-elasticsearch + containers: + - env: + - name: ES_JAVA_OPTS + value: "{{ .Values.FULLTEXTSEARCH_JAVA_OPTIONS | default "-Xms512M -Xmx512M" }}" + - name: FULLTEXTSEARCH_PASSWORD + value: "{{ .Values.FULLTEXTSEARCH_PASSWORD }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + - name: bootstrap.memory_lock + value: "false" + - name: cluster.name + value: nextcloud-aio + - name: discovery.type + value: single-node + - name: http.port + value: "9200" + - name: logger.level + value: WARN + - name: xpack.license.self_generated.type + value: basic + - name: xpack.security.enabled + value: "false" + image: ghcr.io/nextcloud-releases/aio-fulltextsearch:20260122_105751 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 5 + initialDelaySeconds: 60 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 5 + initialDelaySeconds: 60 + periodSeconds: 10 + timeoutSeconds: 5 + name: nextcloud-aio-fulltextsearch + ports: + - containerPort: 9200 + protocol: TCP + volumeMounts: + - mountPath: /usr/share/elasticsearch/data + name: nextcloud-aio-elasticsearch + volumes: + - name: nextcloud-aio-elasticsearch + persistentVolumeClaim: + claimName: nextcloud-aio-elasticsearch +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-service.yaml new file mode 100755 index 00000000..efe474b3 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.FULLTEXTSEARCH_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-fulltextsearch + name: nextcloud-aio-fulltextsearch + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "9200" + port: 9200 + targetPort: 9200 + selector: + io.kompose.service: nextcloud-aio-fulltextsearch +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-deployment.yaml new file mode 100755 index 00000000..af15d4b3 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-deployment.yaml @@ -0,0 +1,69 @@ +{{- if eq .Values.IMAGINARY_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-imaginary + name: nextcloud-aio-imaginary + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-imaginary + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-imaginary + spec: + securityContext: + # The items below only work in pod context + fsGroup: 65534 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 65534 + runAsGroup: 65534 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: IMAGINARY_SECRET + value: "{{ .Values.IMAGINARY_SECRET }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-imaginary:20260122_105751 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-imaginary + ports: + - containerPort: 9000 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-service.yaml new file mode 100755 index 00000000..44a57006 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.IMAGINARY_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-imaginary + name: nextcloud-aio-imaginary + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "9000" + port: 9000 + targetPort: 9000 + selector: + io.kompose.service: nextcloud-aio-imaginary +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-namespace-namespace.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-namespace-namespace.yaml new file mode 100755 index 00000000..212715e9 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-namespace-namespace.yaml @@ -0,0 +1,11 @@ +{{- if and (ne .Values.NAMESPACE "default") (ne .Values.NAMESPACE_DISABLED "yes") }} +apiVersion: v1 +kind: Namespace +metadata: + name: "{{ .Values.NAMESPACE }}" + namespace: "{{ .Values.NAMESPACE }}" + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + labels: + pod-security.kubernetes.io/enforce: restricted + {{- end }} +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-networkpolicy.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-networkpolicy.yaml new file mode 100755 index 00000000..c54f8803 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-networkpolicy.yaml @@ -0,0 +1,36 @@ +{{- if eq .Values.NETWORK_POLICY_ENABLED "yes" }} +# https://github.com/ahmetb/kubernetes-network-policy-recipes/blob/master/04-deny-traffic-from-other-namespaces.md +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + namespace: "{{ .Values.NAMESPACE }}" + name: nextcloud-aio-deny-from-other-namespaces +spec: + podSelector: + matchLabels: + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: {} + egress: + - {} # Allows all egress traffic +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + namespace: "{{ .Values.NAMESPACE }}" + name: nextcloud-aio-webserver-allow +spec: + podSelector: + matchExpressions: + - key: io.kompose.service + operator: In + values: + - nextcloud-aio-apache + policyTypes: + - Ingress + ingress: + - {} # Allows all ingress traffic +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-data-persistentvolumeclaim.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-data-persistentvolumeclaim.yaml new file mode 100755 index 00000000..62794e3b --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-data-persistentvolumeclaim.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-nextcloud-data + name: nextcloud-aio-nextcloud-data + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS_DATA }} + storageClassName: {{ .Values.STORAGE_CLASS_DATA }} + {{- else if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.NEXTCLOUD_DATA_STORAGE_SIZE }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-deployment.yaml new file mode 100755 index 00000000..8b6e8211 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-deployment.yaml @@ -0,0 +1,243 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-nextcloud + name: nextcloud-aio-nextcloud + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-nextcloud + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-nextcloud + spec: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} # AIO-config - do not change this comment! + securityContext: + # The items below only work in pod context + fsGroup: 33 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 33 + runAsGroup: 33 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + {{- end }} # AIO-config - do not change this comment! +# AIO settings start # Do not remove or change this line! + initContainers: + - name: init-volumes + image: ghcr.io/nextcloud-releases/aio-alpine:20260122_105751 + command: + - chmod + - "777" + - /nextcloud-aio-nextcloud + - /nextcloud-aio-nextcloud-trusted-cacerts + volumeMounts: + - name: nextcloud-aio-nextcloud-trusted-cacerts + mountPath: /nextcloud-aio-nextcloud-trusted-cacerts + - name: nextcloud-aio-nextcloud + mountPath: /nextcloud-aio-nextcloud +# AIO settings end # Do not remove or change this line! + containers: + - env: + - name: SMTP_HOST + value: "{{ .Values.SMTP_HOST }}" + - name: SMTP_SECURE + value: "{{ .Values.SMTP_SECURE }}" + - name: SMTP_PORT + value: "{{ .Values.SMTP_PORT }}" + - name: SMTP_AUTHTYPE + value: "{{ .Values.SMTP_AUTHTYPE }}" + - name: SMTP_NAME + value: "{{ .Values.SMTP_NAME }}" + - name: SMTP_PASSWORD + value: "{{ .Values.SMTP_PASSWORD }}" + - name: MAIL_FROM_ADDRESS + value: "{{ .Values.MAIL_FROM_ADDRESS }}" + - name: MAIL_DOMAIN + value: "{{ .Values.MAIL_DOMAIN }}" + - name: SUBSCRIPTION_KEY + value: "{{ .Values.SUBSCRIPTION_KEY }}" + - name: APPS_ALLOWLIST + value: "{{ .Values.APPS_ALLOWLIST }}" + - name: ADDITIONAL_TRUSTED_PROXY + value: "{{ .Values.ADDITIONAL_TRUSTED_PROXY }}" + - name: ADDITIONAL_TRUSTED_DOMAIN + value: "{{ .Values.ADDITIONAL_TRUSTED_DOMAIN }}" + - name: SERVERINFO_TOKEN + value: "{{ .Values.SERVERINFO_TOKEN }}" + - name: NEXTCLOUD_DEFAULT_QUOTA + value: "{{ .Values.NEXTCLOUD_DEFAULT_QUOTA }}" + - name: NEXTCLOUD_SKELETON_DIRECTORY + value: "{{ .Values.NEXTCLOUD_SKELETON_DIRECTORY }}" + - name: NEXTCLOUD_MAINTENANCE_WINDOW + value: "{{ .Values.NEXTCLOUD_MAINTENANCE_WINDOW }}" + - name: ADDITIONAL_APKS + value: "{{ .Values.NEXTCLOUD_ADDITIONAL_APKS }}" + - name: ADDITIONAL_PHP_EXTENSIONS + value: "{{ .Values.NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS }}" + - name: ADMIN_PASSWORD + value: "{{ .Values.NEXTCLOUD_PASSWORD }}" + - name: ADMIN_USER + value: admin + - name: APACHE_HOST + value: nextcloud-aio-apache + - name: APACHE_PORT + value: "{{ .Values.APACHE_PORT }}" + - name: CLAMAV_ENABLED + value: "{{ .Values.CLAMAV_ENABLED }}" + - name: CLAMAV_HOST + value: nextcloud-aio-clamav + - name: COLLABORA_ENABLED + value: "{{ .Values.COLLABORA_ENABLED }}" + - name: COLLABORA_HOST + value: nextcloud-aio-collabora + - name: FULLTEXTSEARCH_ENABLED + value: "{{ .Values.FULLTEXTSEARCH_ENABLED }}" + - name: FULLTEXTSEARCH_HOST + value: nextcloud-aio-fulltextsearch + - name: FULLTEXTSEARCH_INDEX + value: nextcloud-aio + - name: FULLTEXTSEARCH_PASSWORD + value: "{{ .Values.FULLTEXTSEARCH_PASSWORD }}" + - name: FULLTEXTSEARCH_PORT + value: "9200" + - name: FULLTEXTSEARCH_PROTOCOL + value: http + - name: FULLTEXTSEARCH_USER + value: elastic + - name: IMAGINARY_ENABLED + value: "{{ .Values.IMAGINARY_ENABLED }}" + - name: IMAGINARY_HOST + value: nextcloud-aio-imaginary + - name: IMAGINARY_SECRET + value: "{{ .Values.IMAGINARY_SECRET }}" + - name: INSTALL_LATEST_MAJOR + value: "{{ .Values.INSTALL_LATEST_MAJOR }}" + - name: NC_DOMAIN + value: "{{ .Values.NC_DOMAIN }}" + - name: NEXTCLOUD_DATA_DIR + value: /mnt/ncdata + - name: NEXTCLOUD_HOST + value: nextcloud-aio-nextcloud + - name: ONLYOFFICE_ENABLED + value: "{{ .Values.ONLYOFFICE_ENABLED }}" + - name: ONLYOFFICE_HOST + value: nextcloud-aio-onlyoffice + - name: ONLYOFFICE_SECRET + value: "{{ .Values.ONLYOFFICE_SECRET }}" + - name: OVERWRITEPROTOCOL + value: https + - name: PHP_MAX_TIME + value: "{{ .Values.NEXTCLOUD_MAX_TIME }}" + - name: PHP_MEMORY_LIMIT + value: "{{ .Values.NEXTCLOUD_MEMORY_LIMIT }}" + - name: PHP_UPLOAD_LIMIT + value: "{{ .Values.NEXTCLOUD_UPLOAD_LIMIT }}" + - name: POSTGRES_DB + value: nextcloud_database + - name: POSTGRES_HOST + value: nextcloud-aio-database + - name: POSTGRES_PASSWORD + value: "{{ .Values.DATABASE_PASSWORD }}" + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_USER + value: nextcloud + - name: RECORDING_SECRET + value: "{{ .Values.RECORDING_SECRET }}" + - name: REDIS_HOST + value: nextcloud-aio-redis + - name: REDIS_HOST_PASSWORD + value: "{{ .Values.REDIS_PASSWORD }}" + - name: REDIS_PORT + value: "6379" + - name: REMOVE_DISABLED_APPS + value: "{{ .Values.REMOVE_DISABLED_APPS }}" + - name: SIGNALING_SECRET + value: "{{ .Values.SIGNALING_SECRET }}" + - name: STARTUP_APPS + value: "{{ .Values.NEXTCLOUD_STARTUP_APPS }}" + - name: TALK_ENABLED + value: "{{ .Values.TALK_ENABLED }}" + - name: TALK_PORT + value: "{{ .Values.TALK_PORT }}" + - name: TALK_RECORDING_ENABLED + value: "{{ .Values.TALK_RECORDING_ENABLED }}" + - name: TALK_RECORDING_HOST + value: nextcloud-aio-talk-recording + - name: TRUSTED_CACERTS_DIR + value: "{{ .Values.NEXTCLOUD_TRUSTED_CACERTS_DIR }}" + - name: TURN_SECRET + value: "{{ .Values.TURN_SECRET }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + - name: UPDATE_NEXTCLOUD_APPS + value: "{{ .Values.UPDATE_NEXTCLOUD_APPS }}" + - name: WHITEBOARD_ENABLED + value: "{{ .Values.WHITEBOARD_ENABLED }}" + - name: WHITEBOARD_SECRET + value: "{{ .Values.WHITEBOARD_SECRET }}" + image: ghcr.io/nextcloud-releases/aio-nextcloud:20260122_105751 + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} # AIO-config - do not change this comment! + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + {{- end }} # AIO-config - do not change this comment! + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-nextcloud + ports: + - containerPort: 9000 + protocol: TCP + - containerPort: 9001 + protocol: TCP + volumeMounts: + - mountPath: /var/www/html + name: nextcloud-aio-nextcloud + - mountPath: /mnt/ncdata + name: nextcloud-aio-nextcloud-data + - mountPath: /usr/local/share/ca-certificates + name: nextcloud-aio-nextcloud-trusted-cacerts + readOnly: true + terminationGracePeriodSeconds: 600 + volumes: + - name: nextcloud-aio-nextcloud + persistentVolumeClaim: + claimName: nextcloud-aio-nextcloud + - name: nextcloud-aio-nextcloud-data + persistentVolumeClaim: + claimName: nextcloud-aio-nextcloud-data + - name: nextcloud-aio-nextcloud-trusted-cacerts + persistentVolumeClaim: + claimName: nextcloud-aio-nextcloud-trusted-cacerts diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-persistentvolumeclaim.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-persistentvolumeclaim.yaml new file mode 100755 index 00000000..ee55be2a --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-nextcloud + name: nextcloud-aio-nextcloud + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteMany + resources: + requests: + storage: {{ .Values.NEXTCLOUD_STORAGE_SIZE }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-service.yaml new file mode 100755 index 00000000..08ab70f2 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-nextcloud + name: nextcloud-aio-nextcloud + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "9000" + port: 9000 + targetPort: 9000 + - name: "9001" + port: 9001 + targetPort: 9001 + selector: + io.kompose.service: nextcloud-aio-nextcloud diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-trusted-cacerts-persistentvolumeclaim.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-trusted-cacerts-persistentvolumeclaim.yaml new file mode 100755 index 00000000..d18f7a82 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-trusted-cacerts-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-nextcloud-trusted-cacerts + name: nextcloud-aio-nextcloud-trusted-cacerts + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.NEXTCLOUD_TRUSTED_CACERTS_STORAGE_SIZE }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-deployment.yaml new file mode 100755 index 00000000..c8e30d05 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-deployment.yaml @@ -0,0 +1,95 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-notify-push + name: nextcloud-aio-notify-push + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-notify-push + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-notify-push + spec: + securityContext: + # The items below only work in pod context + fsGroup: 33 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 33 + runAsGroup: 33 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: NC_DOMAIN + value: "{{ .Values.NC_DOMAIN }}" + - name: NEXTCLOUD_HOST + value: nextcloud-aio-nextcloud + - name: POSTGRES_DB + value: nextcloud_database + - name: POSTGRES_HOST + value: nextcloud-aio-database + - name: POSTGRES_PASSWORD + value: "{{ .Values.DATABASE_PASSWORD }}" + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_USER + value: nextcloud + - name: REDIS_HOST + value: nextcloud-aio-redis + - name: REDIS_HOST_PASSWORD + value: "{{ .Values.REDIS_PASSWORD }}" + - name: REDIS_PORT + value: "6379" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-notify-push:20260122_105751 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-notify-push + ports: + - containerPort: 7867 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + volumeMounts: + - mountPath: /nextcloud + name: nextcloud-aio-nextcloud + readOnly: true + volumes: + - name: nextcloud-aio-nextcloud + persistentVolumeClaim: + claimName: nextcloud-aio-nextcloud diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-service.yaml new file mode 100755 index 00000000..986d98d4 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-notify-push + name: nextcloud-aio-notify-push + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "7867" + port: 7867 + targetPort: 7867 + selector: + io.kompose.service: nextcloud-aio-notify-push diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-deployment.yaml new file mode 100755 index 00000000..2bb79f19 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-deployment.yaml @@ -0,0 +1,73 @@ +{{- if eq .Values.ONLYOFFICE_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-onlyoffice + name: nextcloud-aio-onlyoffice + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-onlyoffice + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-onlyoffice + spec: + initContainers: + - name: init-volumes + image: ghcr.io/nextcloud-releases/aio-alpine:20260122_105751 + command: + - chmod + - "777" + - /nextcloud-aio-onlyoffice + volumeMounts: + - name: nextcloud-aio-onlyoffice + mountPath: /nextcloud-aio-onlyoffice + containers: + - env: + - name: JWT_ENABLED + value: "true" + - name: JWT_HEADER + value: AuthorizationJwt + - name: JWT_SECRET + value: "{{ .Values.ONLYOFFICE_SECRET }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-onlyoffice:20260122_105751 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-onlyoffice + ports: + - containerPort: 80 + protocol: TCP + volumeMounts: + - mountPath: /var/lib/onlyoffice + name: nextcloud-aio-onlyoffice + volumes: + - name: nextcloud-aio-onlyoffice + persistentVolumeClaim: + claimName: nextcloud-aio-onlyoffice +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-persistentvolumeclaim.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-persistentvolumeclaim.yaml new file mode 100755 index 00000000..80de727b --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-persistentvolumeclaim.yaml @@ -0,0 +1,18 @@ +{{- if eq .Values.ONLYOFFICE_ENABLED "yes" }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-onlyoffice + name: nextcloud-aio-onlyoffice + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.ONLYOFFICE_STORAGE_SIZE }} +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-service.yaml new file mode 100755 index 00000000..5fc10b85 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.ONLYOFFICE_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-onlyoffice + name: nextcloud-aio-onlyoffice + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "80" + port: 80 + targetPort: 80 + selector: + io.kompose.service: nextcloud-aio-onlyoffice +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-deployment.yaml new file mode 100755 index 00000000..28335e64 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-deployment.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-redis + name: nextcloud-aio-redis + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-redis + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-redis + spec: + securityContext: + # The items below only work in pod context + fsGroup: 999 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 999 + runAsGroup: 999 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: REDIS_HOST_PASSWORD + value: "{{ .Values.REDIS_PASSWORD }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-redis:20260122_105751 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-redis + ports: + - containerPort: 6379 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + volumeMounts: + - mountPath: /data + name: nextcloud-aio-redis + volumes: + - name: nextcloud-aio-redis + persistentVolumeClaim: + claimName: nextcloud-aio-redis diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-persistentvolumeclaim.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-persistentvolumeclaim.yaml new file mode 100755 index 00000000..51b4f588 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-redis + name: nextcloud-aio-redis + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.REDIS_STORAGE_SIZE }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-service.yaml new file mode 100755 index 00000000..a6a9a0a5 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-redis + name: nextcloud-aio-redis + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "6379" + port: 6379 + targetPort: 6379 + selector: + io.kompose.service: nextcloud-aio-redis diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-deployment.yaml new file mode 100755 index 00000000..679dd66e --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-deployment.yaml @@ -0,0 +1,87 @@ +{{- if eq .Values.TALK_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-talk + name: nextcloud-aio-talk + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-talk + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-talk + spec: + securityContext: + # The items below only work in pod context + fsGroup: 1000 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: TALK_MAX_STREAM_BITRATE + value: "{{ .Values.TALK_MAX_STREAM_BITRATE }}" + - name: TALK_MAX_SCREEN_BITRATE + value: "{{ .Values.TALK_MAX_SCREEN_BITRATE }}" + - name: INTERNAL_SECRET + value: "{{ .Values.TALK_INTERNAL_SECRET }}" + - name: NC_DOMAIN + value: "{{ .Values.NC_DOMAIN }}" + - name: SIGNALING_SECRET + value: "{{ .Values.SIGNALING_SECRET }}" + - name: TALK_HOST + value: nextcloud-aio-talk + - name: TALK_PORT + value: "{{ .Values.TALK_PORT }}" + - name: TURN_SECRET + value: "{{ .Values.TURN_SECRET }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-talk:20260122_105751 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-talk + ports: + - containerPort: {{ .Values.TALK_PORT }} + protocol: TCP + - containerPort: {{ .Values.TALK_PORT }} + protocol: UDP + - containerPort: 8081 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-deployment.yaml new file mode 100755 index 00000000..8e631656 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-deployment.yaml @@ -0,0 +1,82 @@ +{{- if eq .Values.TALK_RECORDING_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-talk-recording + name: nextcloud-aio-talk-recording + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-talk-recording + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-talk-recording + spec: + securityContext: + # The items below only work in pod context + fsGroup: 122 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 122 + runAsGroup: 122 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: INTERNAL_SECRET + value: "{{ .Values.TALK_INTERNAL_SECRET }}" + - name: NC_DOMAIN + value: "{{ .Values.NC_DOMAIN }}" + - name: RECORDING_SECRET + value: "{{ .Values.RECORDING_SECRET }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-talk-recording:20260122_105751 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-talk-recording + ports: + - containerPort: 1234 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + volumeMounts: + - mountPath: /tmp + name: nextcloud-aio-talk-recording + volumes: + - name: nextcloud-aio-talk-recording + persistentVolumeClaim: + claimName: nextcloud-aio-talk-recording +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-persistentvolumeclaim.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-persistentvolumeclaim.yaml new file mode 100755 index 00000000..59961448 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-persistentvolumeclaim.yaml @@ -0,0 +1,18 @@ +{{- if eq .Values.TALK_RECORDING_ENABLED "yes" }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-talk-recording + name: nextcloud-aio-talk-recording + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.TALK_RECORDING_STORAGE_SIZE }} +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-service.yaml new file mode 100755 index 00000000..87fe0355 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.TALK_RECORDING_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-talk-recording + name: nextcloud-aio-talk-recording + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "1234" + port: 1234 + targetPort: 1234 + selector: + io.kompose.service: nextcloud-aio-talk-recording +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-service.yaml new file mode 100755 index 00000000..65388792 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-service.yaml @@ -0,0 +1,43 @@ +{{- if eq .Values.TALK_ENABLED "yes" }} +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-talk + name: nextcloud-aio-talk-public + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + type: LoadBalancer + ports: + - name: "{{ .Values.TALK_PORT }}" + port: {{ .Values.TALK_PORT }} + targetPort: {{ .Values.TALK_PORT }} + - name: {{ .Values.TALK_PORT }}-udp + port: {{ .Values.TALK_PORT }} + protocol: UDP + targetPort: {{ .Values.TALK_PORT }} + selector: + io.kompose.service: nextcloud-aio-talk +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-talk + name: nextcloud-aio-talk + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "8081" + port: 8081 + targetPort: 8081 + selector: + io.kompose.service: nextcloud-aio-talk +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-deployment.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-deployment.yaml new file mode 100755 index 00000000..5788cfa0 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-deployment.yaml @@ -0,0 +1,81 @@ +{{- if eq .Values.WHITEBOARD_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-whiteboard + name: nextcloud-aio-whiteboard + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-whiteboard + template: + metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-whiteboard + spec: + securityContext: + # The items below only work in pod context + fsGroup: 65534 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 65534 + runAsGroup: 65534 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: BACKUP_DIR + value: /tmp + - name: JWT_SECRET_KEY + value: "{{ .Values.WHITEBOARD_SECRET }}" + - name: NEXTCLOUD_URL + value: https://{{ .Values.NC_DOMAIN }} + - name: REDIS_HOST + value: nextcloud-aio-redis + - name: REDIS_HOST_PASSWORD + value: "{{ .Values.REDIS_PASSWORD }}" + - name: REDIS_PORT + value: "6379" + - name: STORAGE_STRATEGY + value: redis + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-whiteboard:20260122_105751 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-whiteboard + ports: + - containerPort: 3002 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} +{{- end }} diff --git a/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-service.yaml b/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-service.yaml new file mode 100755 index 00000000..299f1ec3 --- /dev/null +++ b/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.WHITEBOARD_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.38.0 (a8f5d1cbd) + labels: + io.kompose.service: nextcloud-aio-whiteboard + name: nextcloud-aio-whiteboard + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "3002" + port: 3002 + targetPort: 3002 + selector: + io.kompose.service: nextcloud-aio-whiteboard +{{- end }} diff --git a/nextcloud-aio-helm-chart/update-helm.sh b/nextcloud-aio-helm-chart/update-helm.sh new file mode 100755 index 00000000..9e5aba86 --- /dev/null +++ b/nextcloud-aio-helm-chart/update-helm.sh @@ -0,0 +1,546 @@ +#!/bin/bash + +[ -z "$1" ] && { echo "Error: Docker tag is not specified. Usage: ./nextcloud-aio-helm-chart/update-helm.sh "; exit 2; } + +DOCKER_TAG="$1" + +# The logic needs the files in ./helm-chart +cp -r ./nextcloud-aio-helm-chart ./helm-chart + +# Clean +rm -f ./helm-chart/values.yaml +rm -rf ./helm-chart/templates + +# Install kompose +curl -L https://github.com/kubernetes/kompose/releases/latest/download/kompose-linux-amd64 -o kompose +chmod +x kompose +sudo mv ./kompose /usr/local/bin/kompose + +# Install yq +sudo snap install yq + +set -ex + +# Conversion of docker-compose +cd manual-install +cp latest.yml latest.yml.backup + +# Additional config +# shellcheck disable=SC1083 +sed -i -E '/^( *- )(NET_RAW|SYS_NICE|MKNOD|SYS_ADMIN|CHOWN|SYS_CHROOT|FOWNER|MAC_OVERRIDE|BLOCK_SUSPEND|AUDIT_READ)$/!s/( *- )([A-Z_]+)$/\1\2=${\2}/' latest.yml +cp sample.conf /tmp/ +sed -i 's|^|export |' /tmp/sample.conf +# shellcheck disable=SC1091 +source /tmp/sample.conf +rm /tmp/sample.conf +sed -i '/OVERWRITEHOST/d' latest.yml +sed -i "s|:latest$|:$DOCKER_TAG|" latest.yml +sed -i "s|\${APACHE_IP_BINDING}:||" latest.yml +sed -i '/APACHE_IP_BINDING/d' latest.yml +sed -i "s|\${APACHE_PORT}:\${APACHE_PORT}/|$APACHE_PORT:$APACHE_PORT/|" latest.yml +sed -i "s|\${TALK_PORT}:\${TALK_PORT}/|$TALK_PORT:$TALK_PORT/|g" latest.yml +sed -i "s|- \${APACHE_PORT}|- $APACHE_PORT|" latest.yml +sed -i "s|- \${TALK_PORT}|- $TALK_PORT|" latest.yml +sed -i "s|\${NEXTCLOUD_DATADIR}|$NEXTCLOUD_DATADIR|" latest.yml +sed -i "s|\${ADDITIONAL_COLLABORA_OPTIONS}|ADDITIONAL_COLLABORA_OPTIONS_PLACEHOLDER|" latest.yml +sed -i "/name: nextcloud-aio/,$ d" latest.yml +sed -i "/NEXTCLOUD_DATADIR/d" latest.yml +sed -i "/\${NEXTCLOUD_MOUNT}/d" latest.yml +sed -i "/^volumes:/a\ \ nextcloud_aio_nextcloud_trusted_cacerts:\n \ \ \ \ name: nextcloud_aio_nextcloud_trusted_cacerts" latest.yml +sed -i "s|\${NEXTCLOUD_TRUSTED_CACERTS_DIR}:|nextcloud_aio_nextcloud_trusted_cacerts:|g#" latest.yml +sed -i 's/\${/{{ .Values./g; s/}/ }}/g' latest.yml +yq -i 'del(.services.[].profiles)' latest.yml +# Delete read_only and tmpfs setting while https://github.com/kubernetes/kubernetes/issues/48912 is not fixed +yq -i 'del(.services.[].read_only)' latest.yml +yq -i 'del(.services.[].tmpfs)' latest.yml +# Remove cap_drop in order to add it later again easier +yq -i 'del(.services.[].cap_drop)' latest.yml +# Remove SYS_NICE for imaginary as it is not supported with RPSS +yq -i 'del(.services."nextcloud-aio-imaginary".cap_add)' latest.yml +# cap SYS_ADMIN is called CAP_SYS_ADMIN in k8s +sed -i "s|- SYS_ADMIN$|- CAP_SYS_ADMIN|" latest.yml + +cat latest.yml +kompose convert -c -f latest.yml --namespace nextcloud-aio-namespace +cd latest + +if [ -f ./templates/manual-install-nextcloud-aio-networkpolicy.yaml ]; then + mv ./templates/manual-install-nextcloud-aio-networkpolicy.yaml ./templates/nextcloud-aio-networkpolicy.yaml +fi +# shellcheck disable=SC1083 +find ./ -name '*networkpolicy.yaml' -exec sed -i "s|manual-install-nextcloud-aio|nextcloud-aio|" \{} \; +cat << EOL > /tmp/initcontainers + initContainers: + - name: init-volumes + image: ghcr.io/nextcloud-releases/aio-alpine:$DOCKER_TAG + command: + - chmod + - "777" + volumeMountsInitContainer: +EOL +cat << EOL > /tmp/initcontainers.database + initContainers: + - name: init-subpath + image: ghcr.io/nextcloud-releases/aio-alpine:$DOCKER_TAG + command: + - mkdir + - "-p" + - /nextcloud-aio-database/data + volumeMounts: + - name: nextcloud-aio-database + mountPath: /nextcloud-aio-database + securityContext: +EOL +cat << EOL > /tmp/initcontainers.clamav + initContainers: + - name: init-subpath + image: ghcr.io/nextcloud-releases/aio-alpine:$DOCKER_TAG + command: + - mkdir + - "-p" + - /nextcloud-aio-clamav/data + volumeMounts: + - name: nextcloud-aio-clamav + mountPath: /nextcloud-aio-clamav + securityContext: +EOL +cat << EOL > /tmp/initcontainers.nextcloud +# AIO settings start # Do not remove or change this line! + initContainers: + - name: init-volumes + image: ghcr.io/nextcloud-releases/aio-alpine:$DOCKER_TAG + command: + - chmod + - "777" + volumeMountsInitContainer: +# AIO settings end # Do not remove or change this line! +EOL + +# shellcheck disable=SC1083 +DEPLOYMENTS="$(find ./ -name '*deployment.yaml')" +mapfile -t DEPLOYMENTS <<< "$DEPLOYMENTS" +for variable in "${DEPLOYMENTS[@]}"; do + if grep -q livenessProbe "$variable"; then + sed -n "/.*livenessProbe/,/timeoutSeconds.*/p" "$variable" > /tmp/liveness.probe + cat /tmp/liveness.probe + sed -i "s|livenessProbe|readinessProbe|" /tmp/liveness.probe + sed -i "/^ image:/r /tmp/liveness.probe" "$variable" + fi + if grep -q volumeMounts "$variable"; then + if echo "$variable" | grep -q database; then + sed -i "/^ spec:/r /tmp/initcontainers.database" "$variable" + elif echo "$variable" | grep -q clamav; then + sed -i "/^ spec:/r /tmp/initcontainers.clamav" "$variable" + elif echo "$variable" | grep -q "nextcloud-deployment.yaml"; then + sed -i "/^ spec:/r /tmp/initcontainers.nextcloud" "$variable" + elif echo "$variable" | grep -q "fulltextsearch" || echo "$variable" | grep -q "onlyoffice" || echo "$variable" | grep -q "collabora"; then + sed -i "/^ spec:/r /tmp/initcontainers" "$variable" + fi + volumeNames="$(grep -A1 mountPath "$variable" | grep -v mountPath | sed 's|.*name: ||' | sed '/^--$/d')" + mapfile -t volumeNames <<< "$volumeNames" + for volumeName in "${volumeNames[@]}"; do + # The Nextcloud container runs as root user and sets the correct permissions automatically for the data-dir if the www-data user cannot write to it + if [ "$volumeName" != "nextcloud-aio-nextcloud-data" ]; then + sed -i "/^.*volumeMountsInitContainer:/i\ \ \ \ \ \ \ \ \ \ \ \ - /$volumeName" "$variable" + sed -i "/volumeMountsInitContainer:/a\ \ \ \ \ \ \ \ \ \ \ \ - name: $volumeName\n\ \ \ \ \ \ \ \ \ \ \ \ \ \ mountPath: /$volumeName" "$variable" + # Workaround for the database volume + if [ "$volumeName" = nextcloud-aio-database ]; then + sed -i "/mountPath: \/var\/lib\/postgresql\/data/a\ \ \ \ \ \ \ \ \ \ \ \ \ \ subPath: data" "$variable" + elif [ "$volumeName" = nextcloud-aio-clamav ]; then + sed -i "/mountPath: \/var\/lib\/clamav/a\ \ \ \ \ \ \ \ \ \ \ \ \ \ subPath: data" "$variable" + fi + + fi + done + sed -i "s|volumeMountsInitContainer:|volumeMounts:|" "$variable" + if grep -q claimName "$variable"; then + claimNames="$(grep claimName "$variable")" + mapfile -t claimNames <<< "$claimNames" + for claimName in "${claimNames[@]}"; do + if grep -A1 "^$claimName$" "$variable" | grep -q "readOnly: true"; then + sed -i "/^$claimName$/{n;d}" "$variable" + fi + done + fi + fi + if grep -q runAsUser "$variable" || echo "$variable" | grep -q "nextcloud-deployment.yaml"; then + if echo "$variable" | grep -q "nextcloud-deployment.yaml"; then + USER=33 + GROUP=33 + echo ' {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} # AIO-config - do not change this comment!' > /tmp/pod.securityContext + else + USER="$(grep runAsUser "$variable" | grep -oP '[0-9]+')" + GROUP="$USER" + rm -f /tmp/pod.securityContext + fi + sed -i "/runAsUser:/d" "$variable" + sed -i "/capabilities:/d" "$variable" + if [ -n "$USER" ]; then + cat << EOL >> /tmp/pod.securityContext + securityContext: + # The items below only work in pod context + fsGroup: $USER + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: $USER + runAsGroup: $GROUP + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} +EOL + if echo "$variable" | grep -q "nextcloud-deployment.yaml"; then + echo " {{- end }} # AIO-config - do not change this comment!" >> /tmp/pod.securityContext + fi + sed -i "/^ spec:$/r /tmp/pod.securityContext" "$variable" + fi + fi +done +# shellcheck disable=SC1083 +find ./ -name '*.yaml' -exec sed -i 's|nextcloud-aio-namespace|"\{\{ .Values.NAMESPACE \}\}"|' \{} \; +# shellcheck disable=SC1083 +find ./ -name '*service.yaml' -exec sed -i "/^status:/,$ d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*deployment.yaml' -exec sed -i "s|manual-install-nextcloud-aio|nextcloud-aio|" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*deployment.yaml' -exec sed -i "/medium: Memory/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*.yaml' -exec sed -i "/kompose.cmd/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*deployment.yaml' -exec sed -i "s|emptyDir:|emptyDir: \{\}|" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*deployment.yaml' -exec sed -i "/hostPort:/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*persistentvolumeclaim.yaml' -exec sed -i "s|ReadOnlyMany|ReadWriteOnce|" \{} \; +# shellcheck disable=SC1083 +find ./ -name 'nextcloud-aio-nextcloud-persistentvolumeclaim.yaml' -exec sed -i "s|ReadWriteOnce|ReadWriteMany|" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*persistentvolumeclaim.yaml' -exec sed -i "/accessModes:/i\ \ {{- if .Values.STORAGE_CLASS }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*persistentvolumeclaim.yaml' -exec sed -i "/accessModes:/i\ \ storageClassName: {{ .Values.STORAGE_CLASS }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*persistentvolumeclaim.yaml' -exec sed -i "/accessModes:/i\ \ {{- end }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name 'nextcloud-aio-nextcloud-data-persistentvolumeclaim.yaml' -exec sed -i "/{{- if .Values.STORAGE_CLASS }}/i\ {{- if .Values.STORAGE_CLASS_DATA }}\n storageClassName: {{ .Values.STORAGE_CLASS_DATA }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name 'nextcloud-aio-nextcloud-data-persistentvolumeclaim.yaml' -exec sed -i "s/{{- if .Values.STORAGE_CLASS }}/{{- else if .Values.STORAGE_CLASS }}/" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*deployment.yaml' -exec sed -i "/restartPolicy:/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*apache*' -exec sed -i "s|$APACHE_PORT|{{ .Values.APACHE_PORT }}|" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*talk*' -exec sed -i "s|$TALK_PORT|{{ .Values.TALK_PORT }}|" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*apache-service.yaml' -exec sed -i "/^spec:/a\ \ type: LoadBalancer" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*talk-service.yaml' -exec sed -i "/^spec:/a\ \ type: LoadBalancer" \{} \; +echo '---' > /tmp/talk-service.copy +# shellcheck disable=SC1083 +find ./ -name '*talk-service.yaml' -exec cat \{} \; >> /tmp/talk-service.copy +sed -i 's|name: nextcloud-aio-talk|name: nextcloud-aio-talk-public|' /tmp/talk-service.copy +# shellcheck disable=SC1083 +INTERNAL_TALK_PORTS="$(find ./ -name '*talk-deployment.yaml' -exec grep -oP 'containerPort: [0-9]+' \{} \;)" +mapfile -t INTERNAL_TALK_PORTS <<< "$INTERNAL_TALK_PORTS" +for port in "${INTERNAL_TALK_PORTS[@]}"; do + port="$(echo "$port" | grep -oP '[0-9]+')" + sed -i "/$port/d" /tmp/talk-service.copy +done +echo '---' >> /tmp/talk-service.copy +# shellcheck disable=SC1083 +find ./ -name '*talk-service.yaml' -exec grep -v '{{ .Values.TALK.*}}\|protocol: UDP\|type: LoadBalancer' \{} \; >> /tmp/talk-service.copy +# shellcheck disable=SC1083 +find ./ -name '*talk-service.yaml' -exec mv /tmp/talk-service.copy \{} \; +# shellcheck disable=SC1083 +find ./ -name '*apache-service.yaml' -exec sed -i "/type: LoadBalancer/a\ \ externalTrafficPolicy: Local" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*service.yaml' -exec sed -i "/^spec:/a\ \ ipFamilyPolicy: PreferDualStack" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*.yaml' -exec sed -i "s|'{{|\"{{|g;s|}}'|}}\"|g" \{} \; +# shellcheck disable=SC1083 +find ./ \( -not -name '*service.yaml' -name '*.yaml' \) -exec sed -i "/^status:/d" \{} \; +# shellcheck disable=SC1083 +find ./ \( -not -name '*persistentvolumeclaim.yaml' -name '*.yaml' \) -exec sed -i "/resources:/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name "*namespace.yaml" -exec sed -i "1i\\{{- if and \(ne .Values.NAMESPACE \"default\"\) \(ne .Values.NAMESPACE_DISABLED \"yes\"\) }}" \{} \; +# Additional config +cat << EOL > /tmp/additional-namespace.config + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + labels: + pod-security.kubernetes.io/enforce: restricted + {{- end }} +EOL +# shellcheck disable=SC1083 +find ./ -name "*namespace.yaml" -exec sed -i "/namespace.*/r /tmp/additional-namespace.config" \{} \; +# shellcheck disable=SC1083 +find ./ -name "*namespace.yaml" -exec sed -i "$ a {{- end }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*.yaml' -exec sed -i "/creationTimestamp: null/d" \{} \; +VOLUMES="$(find ./ -name '*persistentvolumeclaim.yaml' | sed 's|-persistentvolumeclaim.yaml||g;s|.*nextcloud-aio-||g' | sort)" +mapfile -t VOLUMES <<< "$VOLUMES" +for variable in "${VOLUMES[@]}"; do + name="$(echo "$variable" | sed 's|-|_|g' | tr '[:lower:]' '[:upper:]')_STORAGE_SIZE" + VOLUME_VARIABLE+=("$name") + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$variable-persistentvolumeclaim.yaml" -exec sed -i "s|storage: 100Mi|storage: {{ .Values.$name }}|" \{} \; +done + +# Additional config +cat << EOL > /tmp/additional.config + - name: SMTP_HOST + value: "{{ .Values.SMTP_HOST }}" + - name: SMTP_SECURE + value: "{{ .Values.SMTP_SECURE }}" + - name: SMTP_PORT + value: "{{ .Values.SMTP_PORT }}" + - name: SMTP_AUTHTYPE + value: "{{ .Values.SMTP_AUTHTYPE }}" + - name: SMTP_NAME + value: "{{ .Values.SMTP_NAME }}" + - name: SMTP_PASSWORD + value: "{{ .Values.SMTP_PASSWORD }}" + - name: MAIL_FROM_ADDRESS + value: "{{ .Values.MAIL_FROM_ADDRESS }}" + - name: MAIL_DOMAIN + value: "{{ .Values.MAIL_DOMAIN }}" + - name: SUBSCRIPTION_KEY + value: "{{ .Values.SUBSCRIPTION_KEY }}" + - name: APPS_ALLOWLIST + value: "{{ .Values.APPS_ALLOWLIST }}" + - name: ADDITIONAL_TRUSTED_PROXY + value: "{{ .Values.ADDITIONAL_TRUSTED_PROXY }}" + - name: ADDITIONAL_TRUSTED_DOMAIN + value: "{{ .Values.ADDITIONAL_TRUSTED_DOMAIN }}" + - name: SERVERINFO_TOKEN + value: "{{ .Values.SERVERINFO_TOKEN }}" + - name: NEXTCLOUD_DEFAULT_QUOTA + value: "{{ .Values.NEXTCLOUD_DEFAULT_QUOTA }}" + - name: NEXTCLOUD_SKELETON_DIRECTORY + value: "{{ .Values.NEXTCLOUD_SKELETON_DIRECTORY }}" + - name: NEXTCLOUD_MAINTENANCE_WINDOW + value: "{{ .Values.NEXTCLOUD_MAINTENANCE_WINDOW }}" +EOL +# shellcheck disable=SC1083 +find ./ -name '*nextcloud-deployment.yaml' -exec sed -i "/^.*\- env:/r /tmp/additional.config" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*fulltextsearch-deployment.yaml' -exec sed -i 's/{{ .Values.FULLTEXTSEARCH_JAVA_OPTIONS }}/{{ .Values.FULLTEXTSEARCH_JAVA_OPTIONS | default "-Xms512M -Xmx512M" }}/' \{} \; + +# Additional config +cat << EOL > /tmp/additional-apache.config + - name: ADDITIONAL_TRUSTED_DOMAIN + value: "{{ .Values.ADDITIONAL_TRUSTED_DOMAIN }}" +EOL +# shellcheck disable=SC1083 +find ./ -name '*apache-deployment.yaml' -exec sed -i "/^.*\- env:/r /tmp/additional-apache.config" \{} \; + +# Additional config +cat << EOL > /tmp/additional-talk.config + - name: TALK_MAX_STREAM_BITRATE + value: "{{ .Values.TALK_MAX_STREAM_BITRATE }}" + - name: TALK_MAX_SCREEN_BITRATE + value: "{{ .Values.TALK_MAX_SCREEN_BITRATE }}" +EOL +# shellcheck disable=SC1083 +find ./ -name '*talk-deployment.yaml' -exec sed -i "/^.*\- env:/r /tmp/additional-talk.config" \{} \; + +# Additional collabora config +# shellcheck disable=SC1083 +find ./ -name '*collabora-deployment.yaml' -exec sed -i "s/image: ghcr.io.*/IMAGE_PLACEHOLDER/" \{} \; +cat << EOL > /tmp/additional-collabora.config + {{- if contains "--o:support_key=" (join " " (.Values.ADDITIONAL_COLLABORA_OPTIONS | default list)) }} + image: ghcr.io/nextcloud-releases/aio-collabora-online:$DOCKER_TAG + {{- else }} + image: ghcr.io/nextcloud-releases/aio-collabora:$DOCKER_TAG + {{- end }} +EOL +# shellcheck disable=SC1083 +find ./ -name '*collabora-deployment.yaml' -exec sed -i "/IMAGE_PLACEHOLDER/r /tmp/additional-collabora.config" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*collabora-deployment.yaml' -exec sed -i "/IMAGE_PLACEHOLDER/d" \{} \; + +cat << EOL > templates/nextcloud-aio-networkpolicy.yaml +{{- if eq .Values.NETWORK_POLICY_ENABLED "yes" }} +# https://github.com/ahmetb/kubernetes-network-policy-recipes/blob/master/04-deny-traffic-from-other-namespaces.md +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + namespace: "{{ .Values.NAMESPACE }}" + name: nextcloud-aio-deny-from-other-namespaces +spec: + podSelector: + matchLabels: + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: {} + egress: + - {} # Allows all egress traffic +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + namespace: "{{ .Values.NAMESPACE }}" + name: nextcloud-aio-webserver-allow +spec: + podSelector: + matchExpressions: + - key: io.kompose.service + operator: In + values: + - nextcloud-aio-apache + policyTypes: + - Ingress + ingress: + - {} # Allows all ingress traffic +{{- end }} +EOL + +cd ../ +mkdir -p ../helm-chart/ +rm latest/Chart.yaml +rm latest/README.md +mv latest/* ../helm-chart/ +rm -r latest +rm latest.yml +mv latest.yml.backup latest.yml + +# Get version of AIO +AIO_VERSION="$(grep 'Nextcloud AIO ' ../php/templates/includes/aio-version.twig | grep -oP '[0-9]+.[0-9]+.[0-9]+')" +sed -i "s|^version:.*|version: $AIO_VERSION|" ../helm-chart/Chart.yaml + +# Conversion of sample.conf +cp sample.conf /tmp/ +sed -i 's|"||g' /tmp/sample.conf +sed -i 's|=|: |' /tmp/sample.conf +sed -i 's|= |: |' /tmp/sample.conf +sed -i '/^NEXTCLOUD_DATADIR/d' /tmp/sample.conf +sed -i '/^APACHE_IP_BINDING/d' /tmp/sample.conf +sed -i '/^NEXTCLOUD_MOUNT/d' /tmp/sample.conf +sed -i '/_ENABLED.*/s/ yes / "yes" /' /tmp/sample.conf +sed -i '/_ENABLED.*/s/ no / "no" /' /tmp/sample.conf +sed -i 's|^NEXTCLOUD_TRUSTED_CACERTS_DIR: .*|NEXTCLOUD_TRUSTED_CACERTS_DIR: # Setting this to any value allows to automatically import root certificates into the Nextcloud container|' /tmp/sample.conf +sed -i 's|17179869184|"17179869184"|' /tmp/sample.conf +# shellcheck disable=SC2129 +echo "" >> /tmp/sample.conf +# shellcheck disable=SC2129 +echo 'STORAGE_CLASS: # By setting this, you can adjust the storage class for your volumes. This should be a fast storage like SSD backed storage! This storage class must provide RWX and RWO volumes (ReadWriteMany and ReadWriteOnce).' >> /tmp/sample.conf +echo 'STORAGE_CLASS_DATA: # Allows to set a dedicated storage class for the Nextcloud data volume. This can be a bit slower storage than the one above. This storage class must provide RWX volumes (ReadWriteMany). ⚠️ Warning: only set this for new installations, not existing ones!' >> /tmp/sample.conf +for variable in "${VOLUME_VARIABLE[@]}"; do + echo "$variable: 1Gi # You can change the size of the $(echo "$variable" | sed 's|_STORAGE_SIZE||;s|_|-|g' | tr '[:upper:]' '[:lower:]') volume that default to 1Gi with this value" >> /tmp/sample.conf +done +sed -i "s|NEXTCLOUD_STORAGE_SIZE: 1Gi|NEXTCLOUD_STORAGE_SIZE: 5Gi|" /tmp/sample.conf +sed -i "s|NEXTCLOUD_DATA_STORAGE_SIZE: 1Gi|NEXTCLOUD_DATA_STORAGE_SIZE: 5Gi|" /tmp/sample.conf + +# Additional config +cat << ADDITIONAL_CONFIG >> /tmp/sample.conf + +NAMESPACE: default # By changing this, you can adjust the namespace of the installation which allows to install multiple instances on one kubernetes cluster +NAMESPACE_DISABLED: "no" # By setting this to "yes", you can disabled the creation of the namespace so that you can use a pre-created one +NETWORK_POLICY_ENABLED: "no" # By setting this to "yes", you can enable a network policy that limits network access to the same namespace. Except the Web server service which is reachable from all endpoints. +SUBSCRIPTION_KEY: # This allows to set the Nextcloud Enterprise key via ENV +SERVERINFO_TOKEN: # This allows to set the serverinfo app token for monitoring your Nextcloud via the serverinfo app +APPS_ALLOWLIST: # This allows to configure allowed apps that will be shown in Nextcloud's Appstore. You need to enter the app-IDs of the apps here and separate them with spaces. E.g. 'files richdocuments' +ADDITIONAL_TRUSTED_PROXY: # Allows to add one additional ip-address to Nextcloud's trusted proxies and to the Office WOPI-allowlist automatically. Set it e.g. like this: 'your.public.ip-address'. You can also use an ip-range here. +ADDITIONAL_TRUSTED_DOMAIN: # Allows to add one domain to Nextcloud's trusted domains and also generates a certificate automatically for it +NEXTCLOUD_DEFAULT_QUOTA: "10 GB" # Allows to adjust the default quota that will be taken into account in Nextcloud for new users. Setting it to "unlimited" will set it to unlimited +NEXTCLOUD_SKELETON_DIRECTORY: # Allows to adjust the sekeleton dir for Nextcloud. Setting it to "empty" will set the value to an empty string "" which will turn off the setting for new users in Nextcloud. +NEXTCLOUD_MAINTENANCE_WINDOW: # Allows to define the maintenance window for Nextcloud. See https://docs.nextcloud.com/server/stable/admin_manual/configuration_server/background_jobs_configuration.html#parameters for possible values +SMTP_HOST: # (empty by default): The hostname of the SMTP server. +SMTP_SECURE: # (empty by default): Set to 'ssl' to use SSL, or 'tls' to use STARTTLS. +SMTP_PORT: # (default: '465' for SSL and '25' for non-secure connections): Optional port for the SMTP connection. Use '587' for an alternative port for STARTTLS. +SMTP_AUTHTYPE: # (default: 'LOGIN'): The method used for authentication. Use 'PLAIN' if no authentication or STARTLS is required. +SMTP_NAME: # (empty by default): The username for the authentication. +SMTP_PASSWORD: # (empty by default): The password for the authentication. +MAIL_FROM_ADDRESS: # (not set by default): Set the local-part for the 'from' field in the emails sent by Nextcloud. +MAIL_DOMAIN: # (not set by default): Set a different domain for the emails than the domain where Nextcloud is installed. +TALK_MAX_STREAM_BITRATE: "1048576" # This allows to adjust the max stream bitrate of the talk hpb +TALK_MAX_SCREEN_BITRATE: "2097152" # This allows to adjust the max stream bitrate of the talk hpb +ADDITIONAL_CONFIG + +mv /tmp/sample.conf ../helm-chart/values.yaml + +ENABLED_VARIABLES="$(grep -oP '^[A-Z_]+_ENABLED' ../helm-chart/values.yaml)" +mapfile -t ENABLED_VARIABLES <<< "$ENABLED_VARIABLES" + +cd ../helm-chart/ +for variable in "${ENABLED_VARIABLES[@]}"; do + name="$(echo "$variable" | sed 's|_ENABLED||g;s|_|-|g' | tr '[:upper:]' '[:lower:]')" + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-deployment.yaml" -exec sed -i "1i\\{{- if eq .Values.$variable \"yes\" }}" \{} \; + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-deployment.yaml" -exec sed -i "$ a {{- end }}" \{} \; + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-service.yaml" -exec sed -i "1i\\{{- if eq .Values.$variable \"yes\" }}" \{} \; + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-service.yaml" -exec sed -i "$ a {{- end }}" \{} \; + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-persistentvolumeclaim.yaml" -exec sed -i "1i\\{{- if eq .Values.$variable \"yes\" }}" \{} \; + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-persistentvolumeclaim.yaml" -exec sed -i "$ a {{- end }}" \{} \; +done + +# Additional case for FTS volume +# shellcheck disable=SC1083 +find ./ -name "*nextcloud-aio-elasticsearch-persistentvolumeclaim.yaml" -exec sed -i "1i\\{{- if eq .Values.FULLTEXTSEARCH_ENABLED \"yes\" }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name "*nextcloud-aio-elasticsearch-persistentvolumeclaim.yaml" -exec sed -i "$ a {{- end }}" \{} \; + +cat << EOL > /tmp/security.conf + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} +EOL +# shellcheck disable=SC1083 +find ./ \( -not -name '*collabora-deployment.yaml*' -not -name '*apache-deployment.yaml*' -not -name '*onlyoffice-deployment.yaml*' -name "*deployment.yaml" \) -exec sed -i "/^ securityContext:$/r /tmp/security.conf" \{} \; + +# shellcheck disable=SC1083 +find ./ -name '*collabora-deployment.yaml*' -exec sed -i "/ADDITIONAL_COLLABORA_OPTIONS_PLACEHOLDER/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*collabora-deployment.yaml*' -exec sed -i "s/- args:/- args: \{\{ .Values.ADDITIONAL_COLLABORA_OPTIONS | default list | toJson \}\}/" \{} \; + +cat << EOL > /tmp/security.conf + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + add: ["NET_BIND_SERVICE"] +EOL + +# shellcheck disable=SC1083 +find ./ -name '*apache-deployment.yaml*' -exec sed -i "/^ securityContext:$/r /tmp/security.conf" \{} \; + +cat << EOL > /tmp/security.conf + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} # AIO-config - do not change this comment! + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + {{- end }} # AIO-config - do not change this comment! +EOL +# shellcheck disable=SC1083 +find ./ -name '*nextcloud-deployment.yaml*' -exec sed -i "/image: .*nextcloud.*aio-nextcloud:.*/r /tmp/security.conf" \{} \; + +chmod 777 -R ./ + +# Seems like the dir needs to match the name of the chart +cd ../ +rm -rf ./nextcloud-aio-helm-chart +mv ./helm-chart ./nextcloud-aio-helm-chart + +set +ex diff --git a/nextcloud-aio-helm-chart/values.yaml b/nextcloud-aio-helm-chart/values.yaml new file mode 100755 index 00000000..25fb2c92 --- /dev/null +++ b/nextcloud-aio-helm-chart/values.yaml @@ -0,0 +1,75 @@ +DATABASE_PASSWORD: # TODO! This needs to be a unique and good password! +FULLTEXTSEARCH_PASSWORD: # TODO! This needs to be a unique and good password! +IMAGINARY_SECRET: # TODO! This needs to be a unique and good password! +NC_DOMAIN: yourdomain.com # TODO! Needs to be changed to the domain that you want to use for Nextcloud. +NEXTCLOUD_PASSWORD: # TODO! This is the password of the initially created Nextcloud admin with username admin. +ONLYOFFICE_SECRET: # TODO! This needs to be a unique and good password! +RECORDING_SECRET: # TODO! This needs to be a unique and good password! +REDIS_PASSWORD: # TODO! This needs to be a unique and good password! +SIGNALING_SECRET: # TODO! This needs to be a unique and good password! +TALK_INTERNAL_SECRET: # TODO! This needs to be a unique and good password! +TIMEZONE: Europe/Berlin # TODO! This is the timezone that your containers will use. +TURN_SECRET: # TODO! This needs to be a unique and good password! +WHITEBOARD_SECRET: # TODO! This needs to be a unique and good password! + +CLAMAV_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +COLLABORA_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +FULLTEXTSEARCH_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +IMAGINARY_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +ONLYOFFICE_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +TALK_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +TALK_RECORDING_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +WHITEBOARD_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. + +APACHE_MAX_SIZE: "17179869184" # This needs to be an integer and in sync with NEXTCLOUD_UPLOAD_LIMIT +APACHE_PORT: 443 # Changing this to a different value than 443 will allow you to run it behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). +ADDITIONAL_COLLABORA_OPTIONS: ['--o:security.seccomp=true'] # You can add additional collabora options here by using the array syntax. +COLLABORA_DICTIONARIES: de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru # You can change this in order to enable other dictionaries for collabora +FULLTEXTSEARCH_JAVA_OPTIONS: -Xms512M -Xmx512M # Allows to adjust the fulltextsearch java options. +INSTALL_LATEST_MAJOR: no # Setting this to yes will install the latest Major Nextcloud version upon the first installation +NEXTCLOUD_ADDITIONAL_APKS: imagemagick # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value. +NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS: imagick # This allows to add additional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value. +NEXTCLOUD_MAX_TIME: 3600 # This allows to change the upload time limit of the Nextcloud container +NEXTCLOUD_MEMORY_LIMIT: 512M # This allows to change the PHP memory limit of the Nextcloud container +NEXTCLOUD_STARTUP_APPS: deck twofactor_totp tasks calendar contacts notes # Allows to modify the Nextcloud apps that are installed on starting AIO the first time +NEXTCLOUD_TRUSTED_CACERTS_DIR: # Setting this to any value allows to automatically import root certificates into the Nextcloud container +NEXTCLOUD_UPLOAD_LIMIT: 16G # This allows to change the upload limit of the Nextcloud container +REMOVE_DISABLED_APPS: yes # Setting this to no keep Nextcloud apps that are disabled via their switch and not uninstall them if they should be installed in Nextcloud. +TALK_PORT: 3478 # This allows to adjust the port that the talk container is using. It should be set to something higher than 1024! Otherwise it might not work! +UPDATE_NEXTCLOUD_APPS: no # When setting to yes (with quotes), it will automatically update all installed Nextcloud apps upon container startup on saturdays. + +STORAGE_CLASS: # By setting this, you can adjust the storage class for your volumes. This should be a fast storage like SSD backed storage! This storage class must provide RWX and RWO volumes (ReadWriteMany and ReadWriteOnce). +STORAGE_CLASS_DATA: # Allows to set a dedicated storage class for the Nextcloud data volume. This can be a bit slower storage than the one above. This storage class must provide RWX volumes (ReadWriteMany). ⚠️ Warning: only set this for new installations, not existing ones! +APACHE_STORAGE_SIZE: 1Gi # You can change the size of the apache volume that default to 1Gi with this value +CLAMAV_STORAGE_SIZE: 1Gi # You can change the size of the clamav volume that default to 1Gi with this value +DATABASE_STORAGE_SIZE: 1Gi # You can change the size of the database volume that default to 1Gi with this value +DATABASE_DUMP_STORAGE_SIZE: 1Gi # You can change the size of the database-dump volume that default to 1Gi with this value +ELASTICSEARCH_STORAGE_SIZE: 1Gi # You can change the size of the elasticsearch volume that default to 1Gi with this value +NEXTCLOUD_STORAGE_SIZE: 5Gi # You can change the size of the nextcloud volume that default to 1Gi with this value +NEXTCLOUD_DATA_STORAGE_SIZE: 5Gi # You can change the size of the nextcloud-data volume that default to 1Gi with this value +NEXTCLOUD_TRUSTED_CACERTS_STORAGE_SIZE: 1Gi # You can change the size of the nextcloud-trusted-cacerts volume that default to 1Gi with this value +ONLYOFFICE_STORAGE_SIZE: 1Gi # You can change the size of the onlyoffice volume that default to 1Gi with this value +REDIS_STORAGE_SIZE: 1Gi # You can change the size of the redis volume that default to 1Gi with this value +TALK_RECORDING_STORAGE_SIZE: 1Gi # You can change the size of the talk-recording volume that default to 1Gi with this value + +NAMESPACE: default # By changing this, you can adjust the namespace of the installation which allows to install multiple instances on one kubernetes cluster +NAMESPACE_DISABLED: "no" # By setting this to "yes", you can disabled the creation of the namespace so that you can use a pre-created one +NETWORK_POLICY_ENABLED: "no" # By setting this to "yes", you can enable a network policy that limits network access to the same namespace. Except the Web server service which is reachable from all endpoints. +SUBSCRIPTION_KEY: # This allows to set the Nextcloud Enterprise key via ENV +SERVERINFO_TOKEN: # This allows to set the serverinfo app token for monitoring your Nextcloud via the serverinfo app +APPS_ALLOWLIST: # This allows to configure allowed apps that will be shown in Nextcloud's Appstore. You need to enter the app-IDs of the apps here and separate them with spaces. E.g. 'files richdocuments' +ADDITIONAL_TRUSTED_PROXY: # Allows to add one additional ip-address to Nextcloud's trusted proxies and to the Office WOPI-allowlist automatically. Set it e.g. like this: 'your.public.ip-address'. You can also use an ip-range here. +ADDITIONAL_TRUSTED_DOMAIN: # Allows to add one domain to Nextcloud's trusted domains and also generates a certificate automatically for it +NEXTCLOUD_DEFAULT_QUOTA: "10 GB" # Allows to adjust the default quota that will be taken into account in Nextcloud for new users. Setting it to "unlimited" will set it to unlimited +NEXTCLOUD_SKELETON_DIRECTORY: # Allows to adjust the sekeleton dir for Nextcloud. Setting it to "empty" will set the value to an empty string "" which will turn off the setting for new users in Nextcloud. +NEXTCLOUD_MAINTENANCE_WINDOW: # Allows to define the maintenance window for Nextcloud. See https://docs.nextcloud.com/server/stable/admin_manual/configuration_server/background_jobs_configuration.html#parameters for possible values +SMTP_HOST: # (empty by default): The hostname of the SMTP server. +SMTP_SECURE: # (empty by default): Set to 'ssl' to use SSL, or 'tls' to use STARTTLS. +SMTP_PORT: # (default: '465' for SSL and '25' for non-secure connections): Optional port for the SMTP connection. Use '587' for an alternative port for STARTTLS. +SMTP_AUTHTYPE: # (default: 'LOGIN'): The method used for authentication. Use 'PLAIN' if no authentication or STARTLS is required. +SMTP_NAME: # (empty by default): The username for the authentication. +SMTP_PASSWORD: # (empty by default): The password for the authentication. +MAIL_FROM_ADDRESS: # (not set by default): Set the local-part for the 'from' field in the emails sent by Nextcloud. +MAIL_DOMAIN: # (not set by default): Set a different domain for the emails than the domain where Nextcloud is installed. +TALK_MAX_STREAM_BITRATE: "1048576" # This allows to adjust the max stream bitrate of the talk hpb +TALK_MAX_SCREEN_BITRATE: "2097152" # This allows to adjust the max stream bitrate of the talk hpb diff --git a/php/.gitignore b/php/.gitignore deleted file mode 100644 index 2954225b..00000000 --- a/php/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/php/data/configuration.json -/php/data/containers.json - diff --git a/php/.idea/.gitignore b/php/.idea/.gitignore deleted file mode 100644 index 73f69e09..00000000 --- a/php/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/php/.idea/aio.iml b/php/.idea/aio.iml deleted file mode 100644 index 042caa0e..00000000 --- a/php/.idea/aio.iml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/php/.idea/modules.xml b/php/.idea/modules.xml deleted file mode 100644 index 139e985f..00000000 --- a/php/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/php/.idea/php.xml b/php/.idea/php.xml deleted file mode 100644 index 14326088..00000000 --- a/php/.idea/php.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/php/.idea/vcs.xml b/php/.idea/vcs.xml deleted file mode 100644 index 6c0b8635..00000000 --- a/php/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/php/README.md b/php/README.md index f33f44b8..af824818 100644 --- a/php/README.md +++ b/php/README.md @@ -4,13 +4,62 @@ This is the code for the PHP Docker controller. ## How to run -Running this locally requires Docker Engine on the same machine. -If this is the case, just execute the following command: +Running this locally requires : -``` -composer install --no-dev -cd public/ -php -S 0.0.0.0:8080 +### 1. Install the development environment + +This project uses Composer as dependency management software. It is very similar to NPM. +The command to install all dependencies is: + +```bash +composer install ``` -You can then access the web interface at `localhost:8080`. +### 2. Access to docker socket + +The `root` user has all privileges including access to the Docker socket. +But **it is not recommended to launch the local instance with full privileges**, consider the docker group for docker access without being `root`. +See https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user + +### 3. Run a `nextcloud-aio-mastercontainer` container + +This application manages containers, including its own container. +So you need to run a `nextcloud-aio-mastercontainer` container for the application to work properly. + +Here is a command to quickly launch a container : + +```bash +docker run \ +--rm \ +--name nextcloud-aio-mastercontainer \ +--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ +--volume /var/run/docker.sock:/var/run/docker.sock \ +ghcr.io/nextcloud-releases/all-in-one:latest +``` + +### 4. Start your server + +With this command you will launch the server: + +```bash +# Make sure to launch this command with a user having access to the docker socket. +SKIP_DOMAIN_VALIDATION=true composer run dev +``` + +You can then access the web interface at http://localhost:8080. + +Note: You can restart the server by preceding the command with other environment variables. + +## Composer routine + +| Command | Description | +|-----------------------------------------|----------------------------------------| +| `composer run dev` | Starts the development server | +| `composer run psalm` | Run Psalm static analysis | +| `composer run psalm:strict` | Run Psalm static analysis strict | +| `composer run psalm:update-baseline` | Run Psalm with `--update-baseline` arg | +| `composer run lint` | Run PHP Syntax check | +| `composer run lint:twig` | Run Twig Syntax check | +| `composer run php-deprecation-detector` | Run PHP Deprecation Detector | + + diff --git a/php/composer.json b/php/composer.json index 55fecbc6..892bdd5d 100644 --- a/php/composer.json +++ b/php/composer.json @@ -1,26 +1,38 @@ { - "autoload": { - "psr-4": { - "AIO\\": ["src/"] - } - }, - "require": { - "php": "^8.0", - "ext-json": "*", - "ext-sodium": "*", - "ext-curl": "*", - "slim/slim": "4.*", - "php-di/slim-bridge": "^3.1", - "guzzlehttp/guzzle": "^7.3", - "guzzlehttp/psr7": "^2.1.0", - "http-interop/http-factory-guzzle": "^1.2", - "slim/twig-view": "^3.2", - "slim/csrf": "^1.2", - "ext-apcu": "*" - }, - "scripts": { + "autoload": { + "psr-4": { + "AIO\\": ["src/"] + } + }, + "require": { + "php": "8.4.*", + "ext-json": "*", + "ext-sodium": "*", + "ext-curl": "*", + "slim/slim": "^4.11", + "php-di/slim-bridge": "^3.3", + "guzzlehttp/guzzle": "^7.5", + "guzzlehttp/psr7": "^2.4", + "http-interop/http-factory-guzzle": "^1.2", + "slim/twig-view": "^3.3", + "slim/csrf": "^1.3", + "ext-apcu": "*" + }, + "require-dev": { + "sserbin/twig-linter": "@dev", + "vimeo/psalm": "^6.0", + "wapmorgan/php-deprecation-detector": "dev-master" + }, + "scripts": { + "dev": [ + "Composer\\Config::disableProcessTimeout", + "php -S localhost:8080 -t public" + ], "psalm": "psalm --threads=1", - "psalm:update-baseline": "psalm --threads=1 --update-baseline", - "lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l" + "psalm:update-baseline": "psalm --threads=1 --monochrome --no-progress --output-format=text --update-baseline", + "psalm:strict": "psalm --threads=1 --show-info=true", + "lint": "php -l src/*.php src/**/*.php public/index.php", + "lint:twig": "twig-linter lint ./templates", + "php-deprecation-detector": "phpdd scan -n -t 8.4 src/*.php src/**/*.php public/index.php" } } diff --git a/php/composer.lock b/php/composer.lock index 41e9133a..77403624 100644 --- a/php/composer.lock +++ b/php/composer.lock @@ -4,26 +4,26 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "46e4dcf2df4e1a85aba17d664cacd815", + "content-hash": "19598625395cc28e64f15d2719f8f98f", "packages": [ { "name": "guzzlehttp/guzzle", - "version": "7.4.5", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1dd98b0564cb3f6bd16ce683cb755f94c10fbd82", - "reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5", - "guzzlehttp/psr7": "^1.9 || ^2.4", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -32,10 +32,11 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "php-http/client-integration-tests": "^3.0", - "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -45,8 +46,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "7.4-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { @@ -112,7 +114,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.4.5" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -128,38 +130,37 @@ "type": "tidelift" } ], - "time": "2022-06-20T22:16:13+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.1", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.5-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\Promise\\": "src/" } @@ -196,7 +197,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.1" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -212,26 +213,26 @@ "type": "tidelift" } ], - "time": "2021-10-22T20:56:57+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.4.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "13388f00956b1503577598873fffb5ae994b5737" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/13388f00956b1503577598873fffb5ae994b5737", - "reference": "13388f00956b1503577598873fffb5ae994b5737", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.1 || ^2.0", "ralouphie/getallheaders": "^3.0" }, "provide": { @@ -239,17 +240,18 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.8 || ^9.3.10" + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.4-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { @@ -311,7 +313,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.4.0" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -327,20 +329,20 @@ "type": "tidelift" } ], - "time": "2022-06-20T21:43:11+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "http-interop/http-factory-guzzle", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/http-interop/http-factory-guzzle.git", - "reference": "8f06e92b95405216b237521cc64c804dd44c4a81" + "reference": "c2c859ceb05c3f42e710b60555f4c35b6a4a3995" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/8f06e92b95405216b237521cc64c804dd44c4a81", - "reference": "8f06e92b95405216b237521cc64c804dd44c4a81", + "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/c2c859ceb05c3f42e710b60555f4c35b6a4a3995", + "reference": "c2c859ceb05c3f42e710b60555f4c35b6a4a3995", "shasum": "" }, "require": { @@ -383,36 +385,38 @@ ], "support": { "issues": "https://github.com/http-interop/http-factory-guzzle/issues", - "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.0" + "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.1" }, - "time": "2021-07-21T13:50:14+00:00" + "time": "2025-12-15T11:28:16+00:00" }, { "name": "laravel/serializable-closure", - "version": "v1.2.0", + "version": "v2.0.8", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "09f0e9fb61829f628205b7c94906c28740ff9540" + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/09f0e9fb61829f628205b7c94906c28740ff9540", - "reference": "09f0e9fb61829f628205b7c94906c28740ff9540", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b", + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b", "shasum": "" }, "require": { - "php": "^7.3|^8.0" + "php": "^8.1" }, "require-dev": { - "pestphp/pest": "^1.18", - "phpstan/phpstan": "^0.12.98", - "symfony/var-dumper": "^5.3" + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -444,7 +448,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2022-05-16T17:09:47+00:00" + "time": "2026-01-08T16:22:46+00:00" }, { "name": "nikic/fast-route", @@ -498,16 +502,16 @@ }, { "name": "php-di/invoker", - "version": "2.3.3", + "version": "2.3.7", "source": { "type": "git", "url": "https://github.com/PHP-DI/Invoker.git", - "reference": "cd6d9f267d1a3474bdddf1be1da079f01b942786" + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/cd6d9f267d1a3474bdddf1be1da079f01b942786", - "reference": "cd6d9f267d1a3474bdddf1be1da079f01b942786", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1", "shasum": "" }, "require": { @@ -517,7 +521,7 @@ "require-dev": { "athletic/athletic": "~0.1.8", "mnapoli/hard-mode": "~0.3.0", - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.0 || ^10 || ^11 || ^12" }, "type": "library", "autoload": { @@ -541,7 +545,7 @@ ], "support": { "issues": "https://github.com/PHP-DI/Invoker/issues", - "source": "https://github.com/PHP-DI/Invoker/tree/2.3.3" + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.7" }, "funding": [ { @@ -549,43 +553,40 @@ "type": "github" } ], - "time": "2021-12-13T09:22:56+00:00" + "time": "2025-08-30T10:22:22+00:00" }, { "name": "php-di/php-di", - "version": "6.4.0", + "version": "7.1.1", "source": { "type": "git", "url": "https://github.com/PHP-DI/PHP-DI.git", - "reference": "ae0f1b3b03d8b29dff81747063cbfd6276246cc4" + "reference": "f88054cc052e40dbe7b383c8817c19442d480352" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/ae0f1b3b03d8b29dff81747063cbfd6276246cc4", - "reference": "ae0f1b3b03d8b29dff81747063cbfd6276246cc4", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/f88054cc052e40dbe7b383c8817c19442d480352", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352", "shasum": "" }, "require": { - "laravel/serializable-closure": "^1.0", - "php": ">=7.4.0", + "laravel/serializable-closure": "^1.0 || ^2.0", + "php": ">=8.0", "php-di/invoker": "^2.0", - "php-di/phpdoc-reader": "^2.0.1", - "psr/container": "^1.0" + "psr/container": "^1.1 || ^2.0" }, "provide": { "psr/container-implementation": "^1.0" }, "require-dev": { - "doctrine/annotations": "~1.10", - "friendsofphp/php-cs-fixer": "^2.4", - "mnapoli/phpunit-easymock": "^1.2", - "ocramius/proxy-manager": "^2.11.2", - "phpstan/phpstan": "^0.12", - "phpunit/phpunit": "^9.5" + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.6 || ^10 || ^11", + "vimeo/psalm": "^5|^6" }, "suggest": { - "doctrine/annotations": "Install it if you want to use annotations (version ~1.2)", - "ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~2.0)" + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" }, "type": "library", "autoload": { @@ -613,7 +614,7 @@ ], "support": { "issues": "https://github.com/PHP-DI/PHP-DI/issues", - "source": "https://github.com/PHP-DI/PHP-DI/tree/6.4.0" + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.1.1" }, "funding": [ { @@ -625,72 +626,31 @@ "type": "tidelift" } ], - "time": "2022-04-09T16:46:38+00:00" - }, - { - "name": "php-di/phpdoc-reader", - "version": "2.2.1", - "source": { - "type": "git", - "url": "https://github.com/PHP-DI/PhpDocReader.git", - "reference": "66daff34cbd2627740ffec9469ffbac9f8c8185c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PhpDocReader/zipball/66daff34cbd2627740ffec9469ffbac9f8c8185c", - "reference": "66daff34cbd2627740ffec9469ffbac9f8c8185c", - "shasum": "" - }, - "require": { - "php": ">=7.2.0" - }, - "require-dev": { - "mnapoli/hard-mode": "~0.3.0", - "phpunit/phpunit": "^8.5|^9.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpDocReader\\": "src/PhpDocReader" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PhpDocReader parses @var and @param values in PHP docblocks (supports namespaced class names with the same resolution rules as PHP)", - "keywords": [ - "phpdoc", - "reflection" - ], - "support": { - "issues": "https://github.com/PHP-DI/PhpDocReader/issues", - "source": "https://github.com/PHP-DI/PhpDocReader/tree/2.2.1" - }, - "time": "2020-10-12T12:39:22+00:00" + "time": "2025-08-16T11:10:48+00:00" }, { "name": "php-di/slim-bridge", - "version": "3.2.0", + "version": "3.4.1", "source": { "type": "git", "url": "https://github.com/PHP-DI/Slim-Bridge.git", - "reference": "1644a2f31079e92a14cebbf90c7f71ebcbe39ee6" + "reference": "02ab0274a19d104d74561164f8915b62d93f3cf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/Slim-Bridge/zipball/1644a2f31079e92a14cebbf90c7f71ebcbe39ee6", - "reference": "1644a2f31079e92a14cebbf90c7f71ebcbe39ee6", + "url": "https://api.github.com/repos/PHP-DI/Slim-Bridge/zipball/02ab0274a19d104d74561164f8915b62d93f3cf0", + "reference": "02ab0274a19d104d74561164f8915b62d93f3cf0", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", "php-di/invoker": "^2.0.0", - "php-di/php-di": "^6.0.0", + "php-di/php-di": "^6.0|^7.0", "slim/slim": "^4.2.0" }, "require-dev": { "laminas/laminas-diactoros": "^2.1", + "mnapoli/hard-mode": "^0.3.0", "phpunit/phpunit": ">= 7.0 < 10" }, "type": "library", @@ -706,28 +666,33 @@ "description": "PHP-DI integration in Slim", "support": { "issues": "https://github.com/PHP-DI/Slim-Bridge/issues", - "source": "https://github.com/PHP-DI/Slim-Bridge/tree/3.2.0" + "source": "https://github.com/PHP-DI/Slim-Bridge/tree/3.4.1" }, - "time": "2021-11-01T16:14:12+00:00" + "time": "2024-06-19T15:47:45+00:00" }, { "name": "psr/container", - "version": "1.1.2", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { "php": ">=7.4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -754,27 +719,27 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.2" + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2021-11-05T16:50:12+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { "name": "psr/http-client", - "version": "1.0.1", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -794,7 +759,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP clients", @@ -806,27 +771,27 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/master" + "source": "https://github.com/php-fig/http-client" }, - "time": "2020-06-29T06:28:15+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { "name": "psr/http-factory", - "version": "1.0.1", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", - "psr/http-message": "^1.0" + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -846,10 +811,10 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -861,31 +826,31 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2019-04-30T12:38:16+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "2.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -900,7 +865,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -914,27 +879,27 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { "name": "psr/http-server-handler", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-server-handler.git", - "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", - "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", "shasum": "" }, "require": { "php": ">=7.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -954,7 +919,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP server-side request handler", @@ -970,28 +935,27 @@ "server" ], "support": { - "issues": "https://github.com/php-fig/http-server-handler/issues", - "source": "https://github.com/php-fig/http-server-handler/tree/master" + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" }, - "time": "2018-10-30T16:46:14+00:00" + "time": "2023-04-10T20:06:20+00:00" }, { "name": "psr/http-server-middleware", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-server-middleware.git", - "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", - "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", "shasum": "" }, "require": { "php": ">=7.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.0 || ^2.0", "psr/http-server-handler": "^1.0" }, "type": "library", @@ -1012,7 +976,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP server-side middleware", @@ -1028,22 +992,22 @@ ], "support": { "issues": "https://github.com/php-fig/http-server-middleware/issues", - "source": "https://github.com/php-fig/http-server-middleware/tree/master" + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" }, - "time": "2018-10-30T17:12:04+00:00" + "time": "2023-04-11T06:14:47+00:00" }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -1078,9 +1042,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "ralouphie/getallheaders", @@ -1128,30 +1092,28 @@ }, { "name": "slim/csrf", - "version": "1.2.1", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/slimphp/Slim-Csrf.git", - "reference": "ee811a258ecee807846aefc51aabc1963ae0a400" + "reference": "a476a61e38451e138c400f6b4ca96037f3c2dd39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slimphp/Slim-Csrf/zipball/ee811a258ecee807846aefc51aabc1963ae0a400", - "reference": "ee811a258ecee807846aefc51aabc1963ae0a400", + "url": "https://api.github.com/repos/slimphp/Slim-Csrf/zipball/a476a61e38451e138c400f6b4ca96037f3c2dd39", + "reference": "a476a61e38451e138c400f6b4ca96037f3c2dd39", "shasum": "" }, "require": { - "php": "^7.3|^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", + "php": "^7.4 || ^8.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.0 || ^2.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0" }, "require-dev": { - "phpspec/prophecy": "^1.12", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "^3.5.8" + "phpunit/phpunit": "^9.6", + "squizlabs/php_codesniffer": "^3.10" }, "type": "library", "autoload": { @@ -1171,7 +1133,7 @@ } ], "description": "Slim Framework 4 CSRF protection PSR-15 middleware", - "homepage": "http://slimframework.com", + "homepage": "https://www.slimframework.com", "keywords": [ "csrf", "framework", @@ -1180,51 +1142,52 @@ ], "support": { "issues": "https://github.com/slimphp/Slim-Csrf/issues", - "source": "https://github.com/slimphp/Slim-Csrf/tree/1.2.1" + "source": "https://github.com/slimphp/Slim-Csrf/tree/1.5.1" }, - "time": "2021-02-04T15:37:21+00:00" + "time": "2025-11-02T14:58:28+00:00" }, { "name": "slim/slim", - "version": "4.10.0", + "version": "4.15.1", "source": { "type": "git", "url": "https://github.com/slimphp/Slim.git", - "reference": "0dfc7d2fdf2553b361d864d51af3fe8a6ad168b0" + "reference": "887893516557506f254d950425ce7f5387a26970" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slimphp/Slim/zipball/0dfc7d2fdf2553b361d864d51af3fe8a6ad168b0", - "reference": "0dfc7d2fdf2553b361d864d51af3fe8a6ad168b0", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/887893516557506f254d950425ce7f5387a26970", + "reference": "887893516557506f254d950425ce7f5387a26970", "shasum": "" }, "require": { "ext-json": "*", "nikic/fast-route": "^1.3", - "php": "^7.4 || ^8.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "psr/container": "^1.0 || ^2.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "require-dev": { - "adriansuter/php-autoload-override": "^1.2", + "adriansuter/php-autoload-override": "^1.4 || ^2", "ext-simplexml": "*", - "guzzlehttp/psr7": "^2.1", - "httpsoft/http-message": "^1.0", - "httpsoft/http-server-request": "^1.0", - "laminas/laminas-diactoros": "^2.8", - "nyholm/psr7": "^1.5", - "nyholm/psr7-server": "^1.0", - "phpspec/prophecy": "^1.15", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/phpstan": "^1.4", - "phpunit/phpunit": "^9.5", - "slim/http": "^1.2", - "slim/psr7": "^1.5", - "squizlabs/php_codesniffer": "^3.6" + "guzzlehttp/psr7": "^2.6", + "httpsoft/http-message": "^1.1", + "httpsoft/http-server-request": "^1.1", + "laminas/laminas-diactoros": "^2.17 || ^3", + "nyholm/psr7": "^1.8", + "nyholm/psr7-server": "^1.1", + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.1", + "phpstan/phpstan": "^1 || ^2", + "phpunit/phpunit": "^9.6 || ^10 || ^11 || ^12", + "slim/http": "^1.3", + "slim/psr7": "^1.6", + "squizlabs/php_codesniffer": "^3.10", + "vimeo/psalm": "^5 || ^6" }, "suggest": { "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", @@ -1251,17 +1214,17 @@ { "name": "Andrew Smith", "email": "a.smith@silentworks.co.uk", - "homepage": "http://silentworks.co.uk" + "homepage": "https://silentworks.co.uk" }, { "name": "Rob Allen", "email": "rob@akrabat.com", - "homepage": "http://akrabat.com" + "homepage": "https://akrabat.com" }, { "name": "Pierre Berube", "email": "pierre@lgse.com", - "homepage": "http://www.lgse.com" + "homepage": "https://www.lgse.com" }, { "name": "Gabriel Manricks", @@ -1297,35 +1260,35 @@ "type": "tidelift" } ], - "time": "2022-03-14T14:18:23+00:00" + "time": "2025-11-21T12:23:44+00:00" }, { "name": "slim/twig-view", - "version": "3.3.0", + "version": "3.4.1", "source": { "type": "git", "url": "https://github.com/slimphp/Twig-View.git", - "reference": "df6dd6af6bbe28041be49c9fb8470c2e9b70cd98" + "reference": "b4268d87d0e327feba5f88d32031e9123655b909" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slimphp/Twig-View/zipball/df6dd6af6bbe28041be49c9fb8470c2e9b70cd98", - "reference": "df6dd6af6bbe28041be49c9fb8470c2e9b70cd98", + "url": "https://api.github.com/repos/slimphp/Twig-View/zipball/b4268d87d0e327feba5f88d32031e9123655b909", + "reference": "b4268d87d0e327feba5f88d32031e9123655b909", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "psr/http-message": "^1.0", - "slim/slim": "^4.9", - "symfony/polyfill-php81": "^1.23", - "twig/twig": "^3.3" + "psr/http-message": "^1.1 || ^2.0", + "slim/slim": "^4.12", + "symfony/polyfill-php81": "^1.29", + "twig/twig": "^3.11" }, "require-dev": { "phpspec/prophecy-phpunit": "^2.0", - "phpstan/phpstan": "^1.3.0", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^1.10.59", + "phpunit/phpunit": "^9.6 || ^10", "psr/http-factory": "^1.0", - "squizlabs/php_codesniffer": "^3.6" + "squizlabs/php_codesniffer": "^3.9" }, "type": "library", "autoload": { @@ -1360,35 +1323,35 @@ ], "support": { "issues": "https://github.com/slimphp/Twig-View/issues", - "source": "https://github.com/slimphp/Twig-View/tree/3.3.0" + "source": "https://github.com/slimphp/Twig-View/tree/3.4.1" }, - "time": "2022-01-02T05:14:45+00:00" + "time": "2024-09-26T05:42:02+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.0.2", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", - "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=8.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -1413,7 +1376,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -1429,24 +1392,24 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.26.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -1456,12 +1419,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.26-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1495,7 +1455,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -1506,29 +1466,34 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.26.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-iconv": "*", + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -1538,12 +1503,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.26-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1578,7 +1540,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -1589,38 +1551,39 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.26.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1" + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1", - "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.26-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1657,7 +1620,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" }, "funding": [ { @@ -1668,43 +1631,50 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "twig/twig", - "version": "v3.4.2", + "version": "v3.23.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077" + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077", - "reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "psr/container": "^1.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -1737,7 +1707,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.4.2" + "source": "https://github.com/twigphp/Twig/tree/v3.23.0" }, "funding": [ { @@ -1749,22 +1719,3099 @@ "type": "tidelift" } ], - "time": "2022-08-12T06:47:24+00:00" + "time": "2026-01-23T21:00:41+00:00" + } + ], + "packages-dev": [ + { + "name": "amphp/amp", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f", + "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-08-27T21:42:00+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" + }, + { + "name": "amphp/cache", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", + "support": { + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" + }, + { + "name": "amphp/dns", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-19T15:43:40+00:00" + }, + { + "name": "amphp/parallel", + "version": "v2.3.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/parallel.git", + "reference": "296b521137a54d3a02425b464e5aee4c93db2c60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parallel/zipball/296b521137a54d3a02425b464e5aee4c93db2c60", + "reference": "296b521137a54d3a02425b464e5aee4c93db2c60", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "files": [ + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], + "psr-4": { + "Amp\\Parallel\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", + "keywords": [ + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" + ], + "support": { + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.3.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-11-15T06:23:42+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "7b52598c2e9105ebcddf247fc523161581930367" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", + "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T16:33:53+00:00" + }, + { + "name": "amphp/process", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:13:44+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-21T14:33:03+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "danog/advanced-json-rpc", + "version": "v3.2.3", + "source": { + "type": "git", + "url": "https://github.com/danog/php-advanced-json-rpc.git", + "reference": "ae703ea7b4811797a10590b6078de05b3b33dd91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/danog/php-advanced-json-rpc/zipball/ae703ea7b4811797a10590b6078de05b3b33dd91", + "reference": "ae703ea7b4811797a10590b6078de05b3b33dd91", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^5", + "php": ">=8.1", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0 || ^6" + }, + "replace": { + "felixfbecker/php-advanced-json-rpc": "^3" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/danog/php-advanced-json-rpc/issues", + "source": "https://github.com/danog/php-advanced-json-rpc/tree/v3.2.3" + }, + "time": "2026-01-12T21:07:10+00:00" + }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, + "time": "2019-12-04T15:06:13+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "felixfbecker/language-server-protocol", + "version": "v1.5.3", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "LanguageServerProtocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "PHP classes for the Language Server Protocol", + "keywords": [ + "language", + "microsoft", + "php", + "server" + ], + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" + }, + "time": "2024-04-30T00:40:11+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, + { + "name": "league/uri", + "version": "7.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-01-14T17:24:56+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-01-15T06:54:53+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.0" + }, + "time": "2024-09-08T10:20:00+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "2f5cbed597cb261d1ea458f3da3a9ad32e670b1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2f5cbed597cb261d1ea458f3da3a9ad32e670b1e", + "reference": "2f5cbed597cb261d1ea458f3da3a9ad32e670b1e", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.1" + }, + "time": "2026-01-20T15:30:42+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" + }, + "time": "2026-01-06T21:53:42+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, + { + "name": "revolt/event-loop", + "version": "v1.0.8", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/b6fc06dce8e9b523c9946138fa5e62181934f91c", + "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.8" + }, + "time": "2025-08-27T21:33:23+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "spatie/array-to-xml", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/array-to-xml.git", + "reference": "88b2f3852a922dd73177a68938f8eb2ec70c7224" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/88b2f3852a922dd73177a68938f8eb2ec70c7224", + "reference": "88b2f3852a922dd73177a68938f8eb2ec70c7224", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "pestphp/pest": "^1.21", + "spatie/pest-plugin-snapshots": "^1.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spatie\\ArrayToXml\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://freek.dev", + "role": "Developer" + } + ], + "description": "Convert an array to xml", + "homepage": "https://github.com/spatie/array-to-xml", + "keywords": [ + "array", + "convert", + "xml" + ], + "support": { + "source": "https://github.com/spatie/array-to-xml/tree/3.4.4" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-12-15T09:00:41+00:00" + }, + { + "name": "sserbin/twig-linter", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sserbin/twig-linter.git", + "reference": "932c7f1dcc79cd54aa011804d42aa7bbb14a970f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sserbin/twig-linter/zipball/932c7f1dcc79cd54aa011804d42aa7bbb14a970f", + "reference": "932c7f1dcc79cd54aa011804d42aa7bbb14a970f", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "php": "^7.4|^8.0", + "symfony/console": "^5.4 || ^6.1", + "symfony/finder": "^5.4 || ^6.1", + "twig/twig": "^2.5 || ^3" + }, + "require-dev": { + "phpunit/phpunit": "^7.3||^8.2|^9.5", + "squizlabs/php_codesniffer": "^3.3", + "vimeo/psalm": "^4.7 || ^5.8" + }, + "default-branch": true, + "bin": [ + "bin/twig-linter" + ], + "type": "library", + "autoload": { + "psr-4": { + "Sserbin\\TwigLinter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "sserbin", + "email": "sserbin@users.noreply.github.com" + } + ], + "description": "Standalone cli twig linter (based on symfony-bridge-twig)", + "keywords": [ + "lint", + "linter", + "twig" + ], + "support": { + "issues": "https://github.com/sserbin/twig-linter/issues", + "source": "https://github.com/sserbin/twig-linter/tree/3.1.2" + }, + "time": "2025-06-03T06:31:48+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.32", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0bc2199c6c1f05276b05956f1ddc63f6d7eb5fc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0bc2199c6c1f05276b05956f1ddc63f6d7eb5fc3", + "reference": "0bc2199c6c1f05276b05956f1ddc63f6d7eb5fc3", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.32" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-13T08:45:59+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v8.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d937d400b980523dc9ee946bb69972b5e619058d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d", + "reference": "d937d400b980523dc9ee946bb69972b5e619058d", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.0.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-01T09:13:36+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.33", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "24965ca011dac87431729640feef8bcf7b5523e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/24965ca011dac87431729640feef8bcf7b5523e0", + "reference": "24965ca011dac87431729640feef8bcf7b5523e0", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.33" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T13:03:48+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/1c4b10461bf2ec27537b5f36105337262f5f5d6f", + "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-12T10:54:30+00:00" + }, + { + "name": "vimeo/psalm", + "version": "6.14.3", + "source": { + "type": "git", + "url": "https://github.com/vimeo/psalm.git", + "reference": "d0b040a91f280f071c1abcb1b77ce3822058725a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/d0b040a91f280f071c1abcb1b77ce3822058725a", + "reference": "d0b040a91f280f071c1abcb1b77ce3822058725a", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/parallel": "^2.3", + "composer-runtime-api": "^2", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^2.0 || ^3.0", + "danog/advanced-json-rpc": "^3.1", + "dnoegel/php-xdg-base-dir": "^0.1.1", + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*", + "felixfbecker/language-server-protocol": "^1.5.3", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", + "netresearch/jsonmapper": "^5.0", + "nikic/php-parser": "^5.0.0", + "php": "~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3 || ~8.5.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0", + "spatie/array-to-xml": "^2.17.0 || ^3.0", + "symfony/console": "^6.0 || ^7.0 || ^8.0", + "symfony/filesystem": "~6.3.12 || ~6.4.3 || ^7.0.3 || ^8.0", + "symfony/polyfill-php84": "^1.31.0" + }, + "provide": { + "psalm/psalm": "self.version" + }, + "require-dev": { + "amphp/phpunit-util": "^3", + "bamarni/composer-bin-plugin": "^1.4", + "brianium/paratest": "^6.9", + "danog/class-finder": "^0.4.8", + "dg/bypass-finals": "^1.5", + "ext-curl": "*", + "mockery/mockery": "^1.5", + "nunomaduro/mock-final-classes": "^1.1", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpdoc-parser": "^1.6", + "phpunit/phpunit": "^9.6", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.19", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.6", + "symfony/process": "^6.0 || ^7.0 || ^8.0" + }, + "suggest": { + "ext-curl": "In order to send data to shepherd", + "ext-igbinary": "^2.0.5 is required, used to serialize caching data" + }, + "bin": [ + "psalm", + "psalm-language-server", + "psalm-plugin", + "psalm-refactor", + "psalm-review", + "psalter" + ], + "type": "project", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev", + "dev-3.x": "3.x-dev", + "dev-4.x": "4.x-dev", + "dev-5.x": "5.x-dev", + "dev-6.x": "6.x-dev", + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psalm\\": "src/Psalm/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Brown" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" + } + ], + "description": "A static analysis tool for finding errors in PHP applications", + "keywords": [ + "code", + "inspection", + "php", + "static analysis" + ], + "support": { + "docs": "https://psalm.dev/docs", + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm" + }, + "time": "2025-12-23T15:36:48+00:00" + }, + { + "name": "wapmorgan/php-deprecation-detector", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/wapmorgan/PhpDeprecationDetector.git", + "reference": "42cd3786c8971bf80921a3add061cd44df3e73e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wapmorgan/PhpDeprecationDetector/zipball/42cd3786c8971bf80921a3add061cd44df3e73e3", + "reference": "42cd3786c8971bf80921a3add061cd44df3e73e3", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=5.4", + "symfony/console": "^3.4|^4.0|^5.0|^6.0" + }, + "replace": { + "wapmorgan/php-code-fixer": "self.version" + }, + "suggest": { + "ext-json": "Adds ability to store report in JSON format", + "macfja/phar-builder": "To build phar" + }, + "default-branch": true, + "bin": [ + "bin/phpdd" + ], + "type": "package", + "extra": { + "phar-builder": { + "name": "phpdd-dev.phar", + "events": { + "command.package.end": "cp phpdd-dev.phar phpdd-`cat bin/version.txt`.phar && chmod +x phpdd-`cat bin/version.txt`.phar && rm bin/version.txt", + "command.package.start": "git describe --tags > bin/version.txt" + }, + "include": [ + "bin", + "data" + ], + "output-dir": "./", + "compression": "BZip2", + "entry-point": "bin/phpdd" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "wapmorgan\\PhpCodeFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Analyzer of PHP code to search issues with deprecated functionality in newer interpreter versions.", + "support": { + "issues": "https://github.com/wapmorgan/PhpDeprecationDetector/issues", + "source": "https://github.com/wapmorgan/PhpDeprecationDetector/tree/master" + }, + "time": "2024-01-30T21:54:23+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", + "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.1.2" + }, + "time": "2026-01-13T14:02:24+00:00" } ], - "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "sserbin/twig-linter": 20, + "wapmorgan/php-deprecation-detector": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.0", + "php": "8.4.*", "ext-json": "*", "ext-sodium": "*", "ext-curl": "*", "ext-apcu": "*" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/php/containers-schema.json b/php/containers-schema.json index 1a668d38..5ed57e34 100644 --- a/php/containers-schema.json +++ b/php/containers-schema.json @@ -1,57 +1,208 @@ { "type": "object", "description": "AIO containers definition schema", - "additionalProperties": false, "minProperties": 1, + "required": ["aio_services_v1"], "properties": { - "production": { + "aio_services_v1": { "type": "array", "items": { "type": "object", "additionalProperties": false, - "minProperties": 11, + "minProperties": 2, + "required": ["image", "container_name", "image_tag"], "properties": { - "containerName": { - "type": "string" + "image": { + "type": "string", + "minLength": 1, + "pattern": "^(ghcr.io/)?[a-z0-9/-]+$" }, - "dependsOn": { + "expose": { "type": "array", "items": { - "type": "string" + "type": "string", + "pattern": "^([0-9]{1,5})$" } }, - "displayName": { - "type": "string" - }, - "environmentVariables": { + "cap_add": { "type": "array", "items": { - "type": "string" + "type": "string", + "pattern": "^[A-Z_]+$" } }, - "identifier": { - "type": "string" - }, - "internalPorts": { + "cap_drop": { "type": "array", "items": { - "type": "string" + "type": "string", + "pattern": "^[A-Z_]+$" } }, - "maxShutdownTime": { + "depends_on": { + "type": "array", + "items": { + "type": "string", + "pattern": "^nextcloud-aio-[a-z-]+$" + } + }, + "display_name": { + "type": "string", + "pattern": "^[()A-Za-z 0-9-]+$" + }, + "environment": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.*=.*$", + "minlength": 1 + } + }, + "container_name": { + "type": "string", + "pattern": "^nextcloud-aio-[a-z0-9-]+$" + }, + "internal_port": { + "type": "string", + "pattern": "^(([0-9]{1,5})|host|(%[A-Z_]+%))$" + }, + "stop_grace_period": { "type": "integer" }, + "user": { + "type": "string", + "pattern": "^[0-9]{1,6}$" + }, "ports": { "type": "array", "items": { - "type": "string" + "type": "object", + "additionalProperties": false, + "minProperties": 3, + "properties": { + "ip_binding": { + "type": "string", + "pattern": "^((%[A-Z_]+%)|127\\.0\\.0\\.1)?$" + }, + "port_number": { + "type": "string", + "pattern": "^(%[A-Z_]+%|[0-9]{1,5})$" + }, + "protocol": { + "type": "string", + "pattern": "^(tcp|udp)$" + } + } } }, - "restartPolicy": { - "type": "string" + "healthcheck": { + "type": "object", + "additionalProperties": false, + "minProperties": 6, + "properties": { + "interval": { + "type": "string", + "pattern": "^[0-9]+s$" + }, + "timeout": { + "type": "string", + "pattern": "^[0-9]+s$" + }, + "retries": { + "type": "integer" + }, + "start_period": { + "type": "string", + "pattern": "^[0-9]+s$" + }, + "start_interval": { + "type": "string", + "pattern": "^[0-9]+s$" + }, + "test": { + "type": "string", + "pattern": "^.*$" + } + } + }, + "aio_variables": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Z_a-z-]+=.*$" + } + }, + "restart": { + "type": "string", + "pattern": "^unless-stopped$" + }, + "shm_size": { + "type": "integer" }, "secrets": { - "type": "array" + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Z_]+$" + } + }, + "ui_secret": { + "type": "string", + "pattern": "^[A-Z_]+$" + }, + "image_tag": { + "type": "string", + "pattern": "^([a-z0-9.-]+|%AIO_CHANNEL%)$" + }, + "documentation": { + "type": "string", + "pattern": "^https://.*$" + }, + "devices": { + "type": "array", + "items": { + "type": "string", + "pattern": "^/dev/[a-z]+$" + } + }, + "enable_nvidia_gpu": { + "type": "boolean" + }, + "apparmor_unconfined": { + "type": "boolean" + }, + "backup_volumes": { + "type": "array", + "items": { + "type": "string", + "pattern": "^nextcloud_aio_[a-z_]+$" + } + }, + "nextcloud_exec_commands": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(php /var/www/html/occ .*|echo .*|touch .*|mkdir .*)$" + } + }, + "profiles": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z-]+$" + } + }, + "read_only": { + "type": "boolean" + }, + "init": { + "type": "boolean" + }, + "tmpfs": { + "type": "array", + "items": { + "type": "string", + "pattern": "^/[a-z/_0-9-:]+$" + } }, "volumes": { "type": "array", @@ -60,11 +211,13 @@ "additionalProperties": false, "minProperties": 3, "properties": { - "location": { - "type": "string" + "destination": { + "type": "string", + "pattern": "^((/[a-z_/.-]+)|(%[A-Z_]+%))$" }, - "name": { - "type": "string" + "source": { + "type": "string", + "pattern": "^((nextcloud_aio_[a-z_]+)|(%[A-Z_]+%)|(/dev)|(/run/udev))$" }, "writeable": { "type": "boolean" diff --git a/php/containers.json b/php/containers.json index 879c4521..8e9218ac 100644 --- a/php/containers.json +++ b/php/containers.json @@ -1,126 +1,213 @@ { - "production": [ + "aio_services_v1": [ { - "identifier": "nextcloud-aio-apache", - "dependsOn": [ + "container_name": "nextcloud-aio-apache", + "image_tag": "%AIO_CHANNEL%", + "documentation": "https://github.com/nextcloud/all-in-one/discussions/2105", + "depends_on": [ "nextcloud-aio-onlyoffice", "nextcloud-aio-collabora", "nextcloud-aio-talk", + "nextcloud-aio-notify-push", + "nextcloud-aio-whiteboard", "nextcloud-aio-nextcloud" ], - "displayName": "Apache", - "containerName": "nextcloud/aio-apache", + "display_name": "Apache", + "image": "ghcr.io/nextcloud-releases/aio-apache", + "user": "33", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, "ports": [ - "%APACHE_PORT%/tcp" + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "%APACHE_PORT%", + "protocol": "tcp" + }, + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "%APACHE_PORT%", + "protocol": "udp" + } ], - "internalPorts": [ - "%APACHE_PORT%" - ], - "secrets": [], - "environmentVariables": [ + "internal_port": "%APACHE_PORT%", + "environment": [ "NC_DOMAIN=%NC_DOMAIN%", "NEXTCLOUD_HOST=nextcloud-aio-nextcloud", + "APACHE_HOST=nextcloud-aio-apache", "COLLABORA_HOST=nextcloud-aio-collabora", "TALK_HOST=nextcloud-aio-talk", "APACHE_PORT=%APACHE_PORT%", "ONLYOFFICE_HOST=nextcloud-aio-onlyoffice", - "TZ=%TIMEZONE%" + "TZ=%TIMEZONE%", + "APACHE_MAX_SIZE=%APACHE_MAX_SIZE%", + "APACHE_MAX_TIME=%NEXTCLOUD_MAX_TIME%", + "NOTIFY_PUSH_HOST=nextcloud-aio-notify-push", + "WHITEBOARD_HOST=nextcloud-aio-whiteboard" ], "volumes": [ { - "name": "nextcloud_aio_nextcloud", - "location": "/var/www/html", + "source": "nextcloud_aio_nextcloud", + "destination": "/var/www/html", "writeable": false }, { - "name": "nextcloud_aio_apache", - "location": "/mnt/data", + "source": "nextcloud_aio_apache", + "destination": "/mnt/data", "writeable": true } ], - "maxShutdownTime": 10, - "restartPolicy": "unless-stopped" + "restart": "unless-stopped", + "backup_volumes": [ + "nextcloud_aio_nextcloud", + "nextcloud_aio_apache" + ], + "read_only": true, + "tmpfs": [ + "/var/log/supervisord", + "/var/run/supervisord", + "/usr/local/apache2/logs", + "/tmp", + "/home/www-data" + ], + "cap_drop": [ + "NET_RAW" + ] }, { - "identifier": "nextcloud-aio-database", - "dependsOn": [], - "displayName": "Database", - "containerName": "nextcloud/aio-postgresql", - "ports": [], - "internalPorts": [ + "container_name": "nextcloud-aio-database", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Database", + "image": "ghcr.io/nextcloud-releases/aio-postgresql", + "user": "999", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ "5432" ], + "internal_port": "5432", "secrets": [ "DATABASE_PASSWORD" ], "volumes": [ { - "name": "nextcloud_aio_database", - "location": "/var/lib/postgresql/data", + "source": "nextcloud_aio_database", + "destination": "/var/lib/postgresql/data", "writeable": true }, { - "name": "nextcloud_aio_database_dump", - "location": "/mnt/data", + "source": "nextcloud_aio_database_dump", + "destination": "/mnt/data", "writeable": true } ], - "environmentVariables": [ + "environment": [ "POSTGRES_PASSWORD=%DATABASE_PASSWORD%", "POSTGRES_DB=nextcloud_database", "POSTGRES_USER=nextcloud", "TZ=%TIMEZONE%", "PGTZ=%TIMEZONE%" ], - "maxShutdownTime": 1800, - "restartPolicy": "unless-stopped" + "stop_grace_period": 1800, + "restart": "unless-stopped", + "shm_size": 268435456, + "backup_volumes": [ + "nextcloud_aio_database", + "nextcloud_aio_database_dump" + ], + "read_only": true, + "tmpfs": [ + "/var/run/postgresql" + ], + "cap_drop": [ + "NET_RAW" + ] }, { - "identifier": "nextcloud-aio-nextcloud", - "dependsOn": [ + "container_name": "nextcloud-aio-nextcloud", + "image_tag": "%AIO_CHANNEL%", + "depends_on": [ "nextcloud-aio-database", "nextcloud-aio-redis", "nextcloud-aio-clamav", "nextcloud-aio-fulltextsearch", - "nextcloud-aio-imaginary" + "nextcloud-aio-talk-recording", + "nextcloud-aio-imaginary", + "nextcloud-aio-docker-socket-proxy" ], - "displayName": "Nextcloud", - "containerName": "nextcloud/aio-nextcloud", - "ports": [], - "internalPorts": [ - "9000" + "display_name": "Nextcloud", + "image": "ghcr.io/nextcloud-releases/aio-nextcloud", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "9000", + "9001" ], + "internal_port": "9000", "secrets": [ "DATABASE_PASSWORD", "REDIS_PASSWORD", "NEXTCLOUD_PASSWORD", "TURN_SECRET", - "SIGNALING_SECRET" + "SIGNALING_SECRET", + "FULLTEXTSEARCH_PASSWORD", + "IMAGINARY_SECRET", + "WHITEBOARD_SECRET" ], "volumes": [ { - "name": "nextcloud_aio_nextcloud", - "location": "/var/www/html", + "source": "nextcloud_aio_nextcloud", + "destination": "/var/www/html", "writeable": true }, { - "name": "%NEXTCLOUD_DATADIR%", - "location": "/mnt/ncdata", + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/mnt/ncdata", "writeable": true }, { - "name": "%NEXTCLOUD_MOUNT%", - "location": "%NEXTCLOUD_MOUNT%", + "source": "%NEXTCLOUD_MOUNT%", + "destination": "%NEXTCLOUD_MOUNT%", "writeable": true + }, + { + "source": "%NEXTCLOUD_TRUSTED_CACERTS_DIR%", + "destination": "/usr/local/share/ca-certificates", + "writeable": false } ], - "environmentVariables": [ + "environment": [ + "NEXTCLOUD_HOST=nextcloud-aio-nextcloud", "POSTGRES_HOST=nextcloud-aio-database", + "POSTGRES_PORT=5432", "POSTGRES_PASSWORD=%DATABASE_PASSWORD%", "POSTGRES_DB=nextcloud_database", "POSTGRES_USER=nextcloud", "REDIS_HOST=nextcloud-aio-redis", + "REDIS_PORT=6379", "REDIS_HOST_PASSWORD=%REDIS_PASSWORD%", + "APACHE_HOST=nextcloud-aio-apache", + "APACHE_PORT=%APACHE_PORT%", "AIO_TOKEN=%AIO_TOKEN%", "NC_DOMAIN=%NC_DOMAIN%", "ADMIN_USER=admin", @@ -132,6 +219,7 @@ "SIGNALING_SECRET=%SIGNALING_SECRET%", "ONLYOFFICE_SECRET=%ONLYOFFICE_SECRET%", "AIO_URL=%AIO_URL%", + "NC_AIO_VERSION=v%AIO_VERSION%", "NEXTCLOUD_MOUNT=%NEXTCLOUD_MOUNT%", "CLAMAV_ENABLED=%CLAMAV_ENABLED%", "CLAMAV_HOST=nextcloud-aio-clamav", @@ -143,223 +231,483 @@ "UPDATE_NEXTCLOUD_APPS=%UPDATE_NEXTCLOUD_APPS%", "TZ=%TIMEZONE%", "TALK_PORT=%TALK_PORT%", + "TURN_DOMAIN=%TURN_DOMAIN%", "IMAGINARY_ENABLED=%IMAGINARY_ENABLED%", "IMAGINARY_HOST=nextcloud-aio-imaginary", "PHP_UPLOAD_LIMIT=%NEXTCLOUD_UPLOAD_LIMIT%", + "PHP_MEMORY_LIMIT=%NEXTCLOUD_MEMORY_LIMIT%", "FULLTEXTSEARCH_ENABLED=%FULLTEXTSEARCH_ENABLED%", "FULLTEXTSEARCH_HOST=nextcloud-aio-fulltextsearch", - "PHP_MAX_TIME=%NEXTCLOUD_MAX_TIME%" + "FULLTEXTSEARCH_PROTOCOL=http", + "FULLTEXTSEARCH_PORT=9200", + "FULLTEXTSEARCH_USER=elastic", + "FULLTEXTSEARCH_INDEX=nextcloud-aio", + "PHP_MAX_TIME=%NEXTCLOUD_MAX_TIME%", + "TRUSTED_CACERTS_DIR=%NEXTCLOUD_TRUSTED_CACERTS_DIR%", + "STARTUP_APPS=%NEXTCLOUD_STARTUP_APPS%", + "ADDITIONAL_APKS=%NEXTCLOUD_ADDITIONAL_APKS%", + "ADDITIONAL_PHP_EXTENSIONS=%NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS%", + "INSTALL_LATEST_MAJOR=%INSTALL_LATEST_MAJOR%", + "TALK_RECORDING_ENABLED=%TALK_RECORDING_ENABLED%", + "RECORDING_SECRET=%RECORDING_SECRET%", + "TALK_RECORDING_HOST=nextcloud-aio-talk-recording", + "FULLTEXTSEARCH_PASSWORD=%FULLTEXTSEARCH_PASSWORD%", + "DOCKER_SOCKET_PROXY_ENABLED=%DOCKER_SOCKET_PROXY_ENABLED%", + "REMOVE_DISABLED_APPS=%REMOVE_DISABLED_APPS%", + "ADDITIONAL_TRUSTED_PROXY=%CADDY_IP_ADDRESS%", + "THIS_IS_AIO=true", + "IMAGINARY_SECRET=%IMAGINARY_SECRET%", + "WHITEBOARD_SECRET=%WHITEBOARD_SECRET%", + "WHITEBOARD_ENABLED=%WHITEBOARD_ENABLED%" ], - "maxShutdownTime": 10, - "restartPolicy": "unless-stopped" + "stop_grace_period": 600, + "restart": "unless-stopped", + "devices": [ + "/dev/dri" + ], + "enable_nvidia_gpu": true, + "backup_volumes": [ + "nextcloud_aio_nextcloud" + ], + "cap_drop": [ + "NET_RAW" + ] }, { - "identifier": "nextcloud-aio-redis", - "dependsOn": [], - "displayName": "Redis", - "containerName": "nextcloud/aio-redis", - "ports": [], - "internalPorts": [ - "6379" + "container_name": "nextcloud-aio-notify-push", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Notify Push", + "image": "ghcr.io/nextcloud-releases/aio-notify-push", + "user": "33", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "7867" ], - "environmentVariables": [ - "REDIS_HOST_PASSWORD=%REDIS_PASSWORD%", - "TZ=%TIMEZONE%" - ], - "volumes": [], + "internal_port": "7867", "secrets": [ "REDIS_PASSWORD", - "ONLYOFFICE_SECRET" - ], - "maxShutdownTime": 10, - "restartPolicy": "unless-stopped" - }, - { - "identifier": "nextcloud-aio-collabora", - "dependsOn": [], - "displayName": "Collabora", - "containerName": "nextcloud/aio-collabora", - "ports": [], - "internalPorts": [ - "9980" - ], - "environmentVariables": [ - "aliasgroup1=https://%NC_DOMAIN%:443", - "extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:logging.level=warning --o:home_mode.enable=true", - "dictionaries=%COLLABORA_DICTIONARIES%", - "TZ=%TIMEZONE%" - ], - "volumes": [], - "secrets": [], - "maxShutdownTime": 10, - "restartPolicy": "unless-stopped" - }, - { - "identifier": "nextcloud-aio-talk", - "dependsOn": [], - "displayName": "Talk", - "containerName": "nextcloud/aio-talk", - "ports": [ - "%TALK_PORT%/tcp", - "%TALK_PORT%/udp" - ], - "internalPorts": [ - "%TALK_PORT%" - ], - "environmentVariables": [ - "NC_DOMAIN=%NC_DOMAIN%", - "TURN_SECRET=%TURN_SECRET%", - "SIGNALING_SECRET=%SIGNALING_SECRET%", - "JANUS_API_KEY=%JANUS_API_KEY%", - "TZ=%TIMEZONE%", - "TALK_PORT=%TALK_PORT%" - ], - "volumes": [], - "secrets": [ - "TURN_SECRET", - "SIGNALING_SECRET", - "JANUS_API_KEY" - ], - "maxShutdownTime": 10, - "restartPolicy": "unless-stopped" - }, - { - "identifier": "nextcloud-aio-borgbackup", - "dependsOn": [], - "displayName": "Borgbackup", - "containerName": "nextcloud/aio-borgbackup", - "ports": [], - "internalPorts": [], - "environmentVariables": [ - "BORG_PASSWORD=%BORGBACKUP_PASSWORD%", - "BORG_MODE=%BORGBACKUP_MODE%", - "SELECTED_RESTORE_TIME=%SELECTED_RESTORE_TIME%", - "BACKUP_RESTORE_PASSWORD=%BACKUP_RESTORE_PASSWORD%", - "ADDITIONAL_DIRECTORIES_BACKUP=%ADDITIONAL_DIRECTORIES_BACKUP%", - "BORGBACKUP_HOST_LOCATION=%BORGBACKUP_HOST_LOCATION%" + "DATABASE_PASSWORD" ], "volumes": [ { - "name": "nextcloud_aio_backup_cache", - "location": "/root", + "source": "nextcloud_aio_nextcloud", + "destination": "/nextcloud", + "writeable": false + } + ], + "environment": [ + "NC_DOMAIN=%NC_DOMAIN%", + "NEXTCLOUD_HOST=nextcloud-aio-nextcloud", + "TZ=%TIMEZONE%", + "REDIS_HOST=nextcloud-aio-redis", + "REDIS_PORT=6379", + "REDIS_HOST_PASSWORD=%REDIS_PASSWORD%", + "POSTGRES_HOST=nextcloud-aio-database", + "POSTGRES_PORT=5432", + "POSTGRES_PASSWORD=%DATABASE_PASSWORD%", + "POSTGRES_DB=nextcloud_database", + "POSTGRES_USER=nextcloud" + ], + "restart": "unless-stopped", + "read_only": true, + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-redis", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Redis", + "image": "ghcr.io/nextcloud-releases/aio-redis", + "user": "999", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "6379" + ], + "internal_port": "6379", + "environment": [ + "REDIS_HOST_PASSWORD=%REDIS_PASSWORD%", + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "nextcloud_aio_redis", + "destination": "/data", + "writeable": true + } + ], + "secrets": [ + "REDIS_PASSWORD", + "ONLYOFFICE_SECRET", + "RECORDING_SECRET" + ], + "restart": "unless-stopped", + "read_only": true, + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-collabora", + "image_tag": "%AIO_CHANNEL%", + "documentation": "https://github.com/nextcloud/all-in-one/discussions/1358", + "display_name": "Collabora", + "image": "ghcr.io/nextcloud-releases/aio-collabora", + "init": true, + "healthcheck": { + "start_period": "60s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 9 + }, + "expose": [ + "9980" + ], + "internal_port": "9980", + "environment": [ + "aliasgroup1=https://%NC_DOMAIN%:443,http://nextcloud-aio-apache:23973", + "extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:logging.disable_server_audit=true --o:logging.level=warning --o:logging.level_startup=warning --o:welcome.enable=false %COLLABORA_SECCOMP_POLICY% --o:remote_font_config.url=https://%NC_DOMAIN%/apps/richdocuments/settings/fonts.json --o:net.post_allow.host[0]=.+", + "dictionaries=%COLLABORA_DICTIONARIES%", + "TZ=%TIMEZONE%", + "server_name=%NC_DOMAIN%", + "DONT_GEN_SSL_CERT=1" + ], + "restart": "unless-stopped", + "nextcloud_exec_commands": [ + "echo 'Activating Collabora config...'", + "php /var/www/html/occ richdocuments:activate-config --wopi-url='http://nextcloud-aio-apache:23973' --callback-url='http://nextcloud-aio-apache:23973'" + ], + "profiles": [ + "collabora" + ], + "cap_add": [ + "MKNOD", + "SYS_ADMIN", + "SYS_CHROOT", + "FOWNER", + "CHOWN" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-talk", + "image_tag": "%AIO_CHANNEL%", + "documentation": "https://github.com/nextcloud/all-in-one/discussions/1358", + "display_name": "Talk", + "image": "ghcr.io/nextcloud-releases/aio-talk", + "user": "1000", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "ports": [ + { + "ip_binding": "", + "port_number": "%TALK_PORT%", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "%TALK_PORT%", + "protocol": "udp" + } + ], + "expose": [ + "8081" + ], + "internal_port": "%TALK_PORT%", + "environment": [ + "NC_DOMAIN=%NC_DOMAIN%", + "TALK_HOST=nextcloud-aio-talk", + "TURN_SECRET=%TURN_SECRET%", + "SIGNALING_SECRET=%SIGNALING_SECRET%", + "TZ=%TIMEZONE%", + "TALK_PORT=%TALK_PORT%", + "INTERNAL_SECRET=%TALK_INTERNAL_SECRET%" + ], + "secrets": [ + "TURN_SECRET", + "SIGNALING_SECRET", + "TALK_INTERNAL_SECRET" + ], + "restart": "unless-stopped", + "profiles": [ + "talk", + "talk-recording" + ], + "read_only": true, + "tmpfs": [ + "/var/log/supervisord", + "/var/run/supervisord", + "/opt/eturnal/run", + "/conf", + "/tmp" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-talk-recording", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Talk Recording", + "image": "ghcr.io/nextcloud-releases/aio-talk-recording", + "user": "122", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "1234" + ], + "internal_port": "1234", + "environment": [ + "NC_DOMAIN=%NC_DOMAIN%", + "TZ=%TIMEZONE%", + "RECORDING_SECRET=%RECORDING_SECRET%", + "INTERNAL_SECRET=%TALK_INTERNAL_SECRET%" + ], + "volumes": [ + { + "source": "nextcloud_aio_talk_recording", + "destination": "/tmp", + "writeable": true + } + ], + "shm_size": 2147483648, + "secrets": [ + "RECORDING_SECRET", + "TALK_INTERNAL_SECRET" + ], + "restart": "unless-stopped", + "profiles": [ + "talk-recording" + ], + "devices": [ + "/dev/dri" + ], + "enable_nvidia_gpu": true, + "read_only": true, + "tmpfs": [ + "/conf" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-borgbackup", + "image_tag": "%AIO_CHANNEL%", + "image": "ghcr.io/nextcloud-releases/aio-borgbackup", + "init": true, + "environment": [ + "BORG_REMOTE_REPO=%BORGBACKUP_REMOTE_REPO%", + "BORG_PASSWORD=%BORGBACKUP_PASSWORD%", + "BORG_MODE=%BORGBACKUP_MODE%", + "SELECTED_RESTORE_TIME=%SELECTED_RESTORE_TIME%", + "RESTORE_EXCLUDE_PREVIEWS=%RESTORE_EXCLUDE_PREVIEWS%", + "BACKUP_RESTORE_PASSWORD=%BACKUP_RESTORE_PASSWORD%", + "ADDITIONAL_DIRECTORIES_BACKUP=%ADDITIONAL_DIRECTORIES_BACKUP%", + "BORGBACKUP_HOST_LOCATION=%BORGBACKUP_HOST_LOCATION%", + "BORG_HOST_ID=nextcloud-aio-borgbackup", + "BORG_RETENTION_POLICY=%BORG_RETENTION_POLICY%" + ], + "volumes": [ + { + "source": "nextcloud_aio_backup_cache", + "destination": "/root", "writeable": true }, { - "name": "nextcloud_aio_nextcloud", - "location": "/nextcloud_aio_volumes/nextcloud_aio_nextcloud", + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data", "writeable": true }, { - "name": "%NEXTCLOUD_DATADIR%", - "location": "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data", + "source": "nextcloud_aio_mastercontainer", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer", "writeable": true }, { - "name": "nextcloud_aio_database", - "location": "/nextcloud_aio_volumes/nextcloud_aio_database", + "source": "%BORGBACKUP_HOST_LOCATION%", + "destination": "/mnt/borgbackup", "writeable": true }, { - "name": "nextcloud_aio_database_dump", - "location": "/nextcloud_aio_volumes/nextcloud_aio_database_dump", + "source": "nextcloud_aio_elasticsearch", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_elasticsearch", "writeable": true }, { - "name": "nextcloud_aio_apache", - "location": "/nextcloud_aio_volumes/nextcloud_aio_apache", - "writeable": true - }, - { - "name": "nextcloud_aio_mastercontainer", - "location": "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer", - "writeable": true - }, - { - "name": "%BORGBACKUP_HOST_LOCATION%", - "location": "/mnt/borgbackup", + "source": "nextcloud_aio_redis", + "destination": "/mnt/redis", "writeable": true } ], "secrets": [ "BORGBACKUP_PASSWORD" ], - "maxShutdownTime": 10, - "restartPolicy": "" + "devices": [ + "/dev/fuse" + ], + "cap_add": [ + "SYS_ADMIN" + ], + "cap_drop": [ + "NET_RAW" + ], + "apparmor_unconfined": true, + "read_only": true, + "tmpfs": [ + "/tmp", + "/nextcloud_aio_volumes" + ] }, { - "identifier": "nextcloud-aio-watchtower", - "dependsOn": [], - "displayName": "Watchtower", - "containerName": "nextcloud/aio-watchtower", - "ports": [], - "internalPorts": [], - "environmentVariables": [ + "container_name": "nextcloud-aio-watchtower", + "image_tag": "%AIO_CHANNEL%", + "image": "ghcr.io/nextcloud-releases/aio-watchtower", + "init": true, + "environment": [ "CONTAINER_TO_UPDATE=nextcloud-aio-mastercontainer" ], "volumes": [ { - "name": "%DOCKER_SOCKET_PATH%", - "location": "/var/run/docker.sock", + "source": "%WATCHTOWER_DOCKER_SOCKET_PATH%", + "destination": "/var/run/docker.sock", "writeable": false } ], - "secrets": [], - "maxShutdownTime": 10, - "restartPolicy": "" + "read_only": true, + "cap_drop": [ + "NET_RAW" + ] }, { - "dependsOn": [], - "identifier": "nextcloud-aio-domaincheck", - "displayName": "Domaincheck", - "containerName": "nextcloud/aio-domaincheck", + "container_name": "nextcloud-aio-domaincheck", + "image_tag": "%AIO_CHANNEL%", + "image": "ghcr.io/nextcloud-releases/aio-domaincheck", + "init": true, "ports": [ - "%APACHE_PORT%/tcp" + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "%APACHE_PORT%", + "protocol": "tcp" + } ], - "internalPorts": [], - "environmentVariables": [ + "internal_port": "%APACHE_PORT%", + "environment": [ "INSTANCE_ID=%INSTANCE_ID%", "APACHE_PORT=%APACHE_PORT%" ], - "volumes": [], "secrets": [ "INSTANCE_ID" ], - "maxShutdownTime": 1, - "restartPolicy": "" + "stop_grace_period": 1, + "read_only": true, + "tmpfs": [ + "/etc/lighttpd", + "/var/www/domaincheck" + ], + "cap_drop": [ + "NET_RAW" + ] }, { - "identifier": "nextcloud-aio-clamav", - "dependsOn": [], - "displayName": "ClamAV", - "containerName": "nextcloud/aio-clamav", - "ports": [], - "internalPorts": [ + "container_name": "nextcloud-aio-clamav", + "image_tag": "%AIO_CHANNEL%", + "display_name": "ClamAV", + "image": "ghcr.io/nextcloud-releases/aio-clamav", + "user": "100", + "init": false, + "healthcheck": { + "start_period": "60s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 9 + }, + "expose": [ "3310" ], - "environmentVariables": [ - "TZ=%TIMEZONE%" + "internal_port": "3310", + "environment": [ + "TZ=%TIMEZONE%", + "MAX_SIZE=%NEXTCLOUD_UPLOAD_LIMIT%" ], "volumes": [ { - "name": "nextcloud_aio_clamav", - "location": "/var/lib/clamav", + "source": "nextcloud_aio_clamav", + "destination": "/var/lib/clamav", "writeable": true } ], - "secrets": [], - "maxShutdownTime": 10, - "restartPolicy": "unless-stopped" + "restart": "unless-stopped", + "profiles": [ + "clamav" + ], + "read_only": true, + "tmpfs": [ + "/tmp", + "/var/log/clamav", + "/run/clamav", + "/var/log/supervisord", + "/var/run/supervisord" + ], + "cap_drop": [ + "NET_RAW" + ] }, { - "identifier": "nextcloud-aio-onlyoffice", - "dependsOn": [], - "displayName": "OnlyOffice", - "containerName": "nextcloud/aio-onlyoffice", - "ports": [], - "internalPorts": [ + "container_name": "nextcloud-aio-onlyoffice", + "image_tag": "%AIO_CHANNEL%", + "display_name": "OnlyOffice", + "image": "ghcr.io/nextcloud-releases/aio-onlyoffice", + "init": true, + "healthcheck": { + "start_period": "60s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 9 + }, + "expose": [ "80" ], - "environmentVariables": [ + "internal_port": "80", + "environment": [ "TZ=%TIMEZONE%", "JWT_ENABLED=true", "JWT_HEADER=AuthorizationJwt", @@ -367,58 +715,182 @@ ], "volumes": [ { - "name": "nextcloud_aio_onlyoffice", - "location": "/var/lib/onlyoffice", + "source": "nextcloud_aio_onlyoffice", + "destination": "/var/lib/onlyoffice", "writeable": true } ], "secrets": [ "ONLYOFFICE_SECRET" ], - "maxShutdownTime": 10, - "restartPolicy": "unless-stopped" + "restart": "unless-stopped", + "profiles": [ + "onlyoffice" + ], + "cap_drop": [ + "NET_RAW" + ] }, { - "identifier": "nextcloud-aio-imaginary", - "dependsOn": [], - "displayName": "Imaginary", - "containerName": "nextcloud/aio-imaginary", - "ports": [], - "internalPorts": [ + "container_name": "nextcloud-aio-imaginary", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Imaginary", + "image": "ghcr.io/nextcloud-releases/aio-imaginary", + "user": "65534", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ "9000" ], - "environmentVariables": [ - "TZ=%TIMEZONE%" + "internal_port": "9000", + "environment": [ + "TZ=%TIMEZONE%", + "IMAGINARY_SECRET=%IMAGINARY_SECRET%" ], - "volumes": [], - "secrets": [], - "maxShutdownTime": 10, - "restartPolicy": "unless-stopped" + "restart": "unless-stopped", + "cap_add": [ + "SYS_NICE" + ], + "cap_drop": [ + "NET_RAW" + ], + "profiles": [ + "imaginary" + ], + "read_only": true, + "tmpfs": [ + "/tmp" + ], + "secrets": [ + "IMAGINARY_SECRET" + ] }, { - "identifier": "nextcloud-aio-fulltextsearch", - "dependsOn": [], - "displayName": "Fulltextsearch", - "containerName": "nextcloud/aio-fulltextsearch", - "ports": [], - "internalPorts": [ + "container_name": "nextcloud-aio-fulltextsearch", + "image_tag": "%AIO_CHANNEL%", + "documentation": "https://github.com/nextcloud/all-in-one/discussions/1709", + "display_name": "Fulltextsearch", + "image": "ghcr.io/nextcloud-releases/aio-fulltextsearch", + "init": false, + "healthcheck": { + "start_period": "60s", + "test": "/healthcheck.sh", + "interval": "10s", + "timeout": "5s", + "start_interval": "5s", + "retries": 5 + }, + "expose": [ "9200" ], - "environmentVariables": [ + "internal_port": "9200", + "environment": [ "TZ=%TIMEZONE%", + "ES_JAVA_OPTS=%FULLTEXTSEARCH_JAVA_OPTIONS%", + "bootstrap.memory_lock=false", + "cluster.name=nextcloud-aio", "discovery.type=single-node", - "ES_JAVA_OPTS=-Xms1024M -Xmx1024M" + "logger.level=WARN", + "http.port=9200", + "xpack.license.self_generated.type=basic", + "xpack.security.enabled=false", + "FULLTEXTSEARCH_PASSWORD=%FULLTEXTSEARCH_PASSWORD%" ], "volumes": [ { - "name": "nextcloud_aio_elasticsearch", - "location": "/usr/share/elasticsearch/data", + "source": "nextcloud_aio_elasticsearch", + "destination": "/usr/share/elasticsearch/data", "writeable": true } ], - "secrets": [], - "maxShutdownTime": 10, - "restartPolicy": "unless-stopped" + "restart": "unless-stopped", + "profiles": [ + "fulltextsearch" + ], + "secrets": [ + "FULLTEXTSEARCH_PASSWORD" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-docker-socket-proxy", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Docker Socket Proxy", + "image": "ghcr.io/nextcloud-releases/aio-docker-socket-proxy", + "init": true, + "internal_port": "2375", + "environment": [ + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "%WATCHTOWER_DOCKER_SOCKET_PATH%", + "destination": "/var/run/docker.sock", + "writeable": false + } + ], + "restart": "unless-stopped", + "read_only": true, + "tmpfs": [ + "/tmp" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-whiteboard", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Whiteboard", + "image": "ghcr.io/nextcloud-releases/aio-whiteboard", + "user": "65534", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "3002" + ], + "tmpfs": [ + "/tmp" + ], + "internal_port": "3002", + "environment": [ + "TZ=%TIMEZONE%", + "NEXTCLOUD_URL=https://%NC_DOMAIN%", + "JWT_SECRET_KEY=%WHITEBOARD_SECRET%", + "STORAGE_STRATEGY=redis", + "REDIS_HOST=nextcloud-aio-redis", + "REDIS_PORT=6379", + "REDIS_HOST_PASSWORD=%REDIS_PASSWORD%", + "BACKUP_DIR=/tmp" + ], + "secrets": [ + "WHITEBOARD_SECRET", + "REDIS_PASSWORD" + ], + "restart": "unless-stopped", + "profiles": [ + "whiteboard" + ], + "read_only": true, + "cap_drop": [ + "NET_RAW" + ] } ] } diff --git a/php/cool-seccomp-profile.json b/php/cool-seccomp-profile.json new file mode 100644 index 00000000..ed2320ca --- /dev/null +++ b/php/cool-seccomp-profile.json @@ -0,0 +1,844 @@ +{ + "defaultAction": "SCMP_ACT_ERRNO", + "defaultErrnoRet": 1, + "archMap": [ + { + "architecture": "SCMP_ARCH_X86_64", + "subArchitectures": [ + "SCMP_ARCH_X86", + "SCMP_ARCH_X32" + ] + }, + { + "architecture": "SCMP_ARCH_AARCH64", + "subArchitectures": [ + "SCMP_ARCH_ARM" + ] + }, + { + "architecture": "SCMP_ARCH_MIPS64", + "subArchitectures": [ + "SCMP_ARCH_MIPS", + "SCMP_ARCH_MIPS64N32" + ] + }, + { + "architecture": "SCMP_ARCH_MIPS64N32", + "subArchitectures": [ + "SCMP_ARCH_MIPS", + "SCMP_ARCH_MIPS64" + ] + }, + { + "architecture": "SCMP_ARCH_MIPSEL64", + "subArchitectures": [ + "SCMP_ARCH_MIPSEL", + "SCMP_ARCH_MIPSEL64N32" + ] + }, + { + "architecture": "SCMP_ARCH_MIPSEL64N32", + "subArchitectures": [ + "SCMP_ARCH_MIPSEL", + "SCMP_ARCH_MIPSEL64" + ] + }, + { + "architecture": "SCMP_ARCH_S390X", + "subArchitectures": [ + "SCMP_ARCH_S390" + ] + }, + { + "architecture": "SCMP_ARCH_RISCV64", + "subArchitectures": null + } + ], + "syscalls": [ + { + "names": [ + "unshare", + "mount", + "setns", + "clone", + "chroot", + "umount2" + ], + "action": "SCMP_ACT_ALLOW" + }, + { + "names": [ + "accept", + "accept4", + "access", + "adjtimex", + "alarm", + "bind", + "brk", + "cachestat", + "capget", + "capset", + "chdir", + "chmod", + "chown", + "chown32", + "clock_adjtime", + "clock_adjtime64", + "clock_getres", + "clock_getres_time64", + "clock_gettime", + "clock_gettime64", + "clock_nanosleep", + "clock_nanosleep_time64", + "close", + "close_range", + "connect", + "copy_file_range", + "creat", + "dup", + "dup2", + "dup3", + "epoll_create", + "epoll_create1", + "epoll_ctl", + "epoll_ctl_old", + "epoll_pwait", + "epoll_pwait2", + "epoll_wait", + "epoll_wait_old", + "eventfd", + "eventfd2", + "execve", + "execveat", + "exit", + "exit_group", + "faccessat", + "faccessat2", + "fadvise64", + "fadvise64_64", + "fallocate", + "fanotify_mark", + "fchdir", + "fchmod", + "fchmodat", + "fchmodat2", + "fchown", + "fchown32", + "fchownat", + "fcntl", + "fcntl64", + "fdatasync", + "fgetxattr", + "flistxattr", + "flock", + "fork", + "fremovexattr", + "fsetxattr", + "fstat", + "fstat64", + "fstatat64", + "fstatfs", + "fstatfs64", + "fsync", + "ftruncate", + "ftruncate64", + "futex", + "futex_requeue", + "futex_time64", + "futex_wait", + "futex_waitv", + "futex_wake", + "futimesat", + "getcpu", + "getcwd", + "getdents", + "getdents64", + "getegid", + "getegid32", + "geteuid", + "geteuid32", + "getgid", + "getgid32", + "getgroups", + "getgroups32", + "getitimer", + "getpeername", + "getpgid", + "getpgrp", + "getpid", + "getppid", + "getpriority", + "getrandom", + "getresgid", + "getresgid32", + "getresuid", + "getresuid32", + "getrlimit", + "get_robust_list", + "getrusage", + "getsid", + "getsockname", + "getsockopt", + "get_thread_area", + "gettid", + "gettimeofday", + "getuid", + "getuid32", + "getxattr", + "inotify_add_watch", + "inotify_init", + "inotify_init1", + "inotify_rm_watch", + "io_cancel", + "ioctl", + "io_destroy", + "io_getevents", + "io_pgetevents", + "io_pgetevents_time64", + "ioprio_get", + "ioprio_set", + "io_setup", + "io_submit", + "ipc", + "kill", + "landlock_add_rule", + "landlock_create_ruleset", + "landlock_restrict_self", + "lchown", + "lchown32", + "lgetxattr", + "link", + "linkat", + "listen", + "listxattr", + "llistxattr", + "_llseek", + "lremovexattr", + "lseek", + "lsetxattr", + "lstat", + "lstat64", + "madvise", + "map_shadow_stack", + "membarrier", + "memfd_create", + "memfd_secret", + "mincore", + "mkdir", + "mkdirat", + "mknod", + "mknodat", + "mlock", + "mlock2", + "mlockall", + "mmap", + "mmap2", + "mprotect", + "mq_getsetattr", + "mq_notify", + "mq_open", + "mq_timedreceive", + "mq_timedreceive_time64", + "mq_timedsend", + "mq_timedsend_time64", + "mq_unlink", + "mremap", + "msgctl", + "msgget", + "msgrcv", + "msgsnd", + "msync", + "munlock", + "munlockall", + "munmap", + "name_to_handle_at", + "nanosleep", + "newfstatat", + "_newselect", + "open", + "openat", + "openat2", + "pause", + "pidfd_open", + "pidfd_send_signal", + "pipe", + "pipe2", + "pkey_alloc", + "pkey_free", + "pkey_mprotect", + "poll", + "ppoll", + "ppoll_time64", + "prctl", + "pread64", + "preadv", + "preadv2", + "prlimit64", + "process_mrelease", + "pselect6", + "pselect6_time64", + "pwrite64", + "pwritev", + "pwritev2", + "read", + "readahead", + "readlink", + "readlinkat", + "readv", + "recv", + "recvfrom", + "recvmmsg", + "recvmmsg_time64", + "recvmsg", + "remap_file_pages", + "removexattr", + "rename", + "renameat", + "renameat2", + "restart_syscall", + "rmdir", + "rseq", + "rt_sigaction", + "rt_sigpending", + "rt_sigprocmask", + "rt_sigqueueinfo", + "rt_sigreturn", + "rt_sigsuspend", + "rt_sigtimedwait", + "rt_sigtimedwait_time64", + "rt_tgsigqueueinfo", + "sched_getaffinity", + "sched_getattr", + "sched_getparam", + "sched_get_priority_max", + "sched_get_priority_min", + "sched_getscheduler", + "sched_rr_get_interval", + "sched_rr_get_interval_time64", + "sched_setaffinity", + "sched_setattr", + "sched_setparam", + "sched_setscheduler", + "sched_yield", + "seccomp", + "select", + "semctl", + "semget", + "semop", + "semtimedop", + "semtimedop_time64", + "send", + "sendfile", + "sendfile64", + "sendmmsg", + "sendmsg", + "sendto", + "setfsgid", + "setfsgid32", + "setfsuid", + "setfsuid32", + "setgid", + "setgid32", + "setgroups", + "setgroups32", + "setitimer", + "setpgid", + "setpriority", + "setregid", + "setregid32", + "setresgid", + "setresgid32", + "setresuid", + "setresuid32", + "setreuid", + "setreuid32", + "setrlimit", + "set_robust_list", + "setsid", + "setsockopt", + "set_thread_area", + "set_tid_address", + "setuid", + "setuid32", + "setxattr", + "shmat", + "shmctl", + "shmdt", + "shmget", + "shutdown", + "sigaltstack", + "signalfd", + "signalfd4", + "sigprocmask", + "sigreturn", + "socketcall", + "socketpair", + "splice", + "stat", + "stat64", + "statfs", + "statfs64", + "statx", + "symlink", + "symlinkat", + "sync", + "sync_file_range", + "syncfs", + "sysinfo", + "tee", + "tgkill", + "time", + "timer_create", + "timer_delete", + "timer_getoverrun", + "timer_gettime", + "timer_gettime64", + "timer_settime", + "timer_settime64", + "timerfd_create", + "timerfd_gettime", + "timerfd_gettime64", + "timerfd_settime", + "timerfd_settime64", + "times", + "tkill", + "truncate", + "truncate64", + "ugetrlimit", + "umask", + "uname", + "unlink", + "unlinkat", + "utime", + "utimensat", + "utimensat_time64", + "utimes", + "vfork", + "vmsplice", + "wait4", + "waitid", + "waitpid", + "write", + "writev" + ], + "action": "SCMP_ACT_ALLOW" + }, + { + "names": [ + "process_vm_readv", + "process_vm_writev", + "ptrace" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "minKernel": "4.8" + } + }, + { + "names": [ + "socket" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 40, + "op": "SCMP_CMP_NE" + } + ] + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 0, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 8, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 131072, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 131080, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 4294967295, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": [ + "sync_file_range2", + "swapcontext" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": [ + "ppc64le" + ] + } + }, + { + "names": [ + "arm_fadvise64_64", + "arm_sync_file_range", + "sync_file_range2", + "breakpoint", + "cacheflush", + "set_tls" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": [ + "arm", + "arm64" + ] + } + }, + { + "names": [ + "arch_prctl" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": [ + "amd64", + "x32" + ] + } + }, + { + "names": [ + "modify_ldt" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": [ + "amd64", + "x32", + "x86" + ] + } + }, + { + "names": [ + "s390_pci_mmio_read", + "s390_pci_mmio_write", + "s390_runtime_instr" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": [ + "s390", + "s390x" + ] + } + }, + { + "names": [ + "riscv_flush_icache" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": [ + "riscv64" + ] + } + }, + { + "names": [ + "open_by_handle_at" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_DAC_READ_SEARCH" + ] + } + }, + { + "names": [ + "bpf", + "clone", + "clone3", + "fanotify_init", + "fsconfig", + "fsmount", + "fsopen", + "fspick", + "lookup_dcookie", + "mount", + "mount_setattr", + "move_mount", + "open_tree", + "perf_event_open", + "quotactl", + "quotactl_fd", + "setdomainname", + "sethostname", + "setns", + "syslog", + "umount", + "umount2", + "unshare" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_SYS_ADMIN" + ] + } + }, + { + "names": [ + "clone" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 2114060288, + "op": "SCMP_CMP_MASKED_EQ" + } + ], + "excludes": { + "caps": [ + "CAP_SYS_ADMIN" + ], + "arches": [ + "s390", + "s390x" + ] + } + }, + { + "names": [ + "clone" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 1, + "value": 2114060288, + "op": "SCMP_CMP_MASKED_EQ" + } + ], + "comment": "s390 parameter ordering for clone is different", + "includes": { + "arches": [ + "s390", + "s390x" + ] + }, + "excludes": { + "caps": [ + "CAP_SYS_ADMIN" + ] + } + }, + { + "names": [ + "clone3" + ], + "action": "SCMP_ACT_ERRNO", + "errnoRet": 38, + "excludes": { + "caps": [ + "CAP_SYS_ADMIN" + ] + } + }, + { + "names": [ + "reboot" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_SYS_BOOT" + ] + } + }, + { + "names": [ + "chroot" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_SYS_CHROOT" + ] + } + }, + { + "names": [ + "delete_module", + "init_module", + "finit_module" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_SYS_MODULE" + ] + } + }, + { + "names": [ + "acct" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_SYS_PACCT" + ] + } + }, + { + "names": [ + "kcmp", + "pidfd_getfd", + "process_madvise", + "process_vm_readv", + "process_vm_writev", + "ptrace" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_SYS_PTRACE" + ] + } + }, + { + "names": [ + "iopl", + "ioperm" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_SYS_RAWIO" + ] + } + }, + { + "names": [ + "settimeofday", + "stime", + "clock_settime", + "clock_settime64" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_SYS_TIME" + ] + } + }, + { + "names": [ + "vhangup" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_SYS_TTY_CONFIG" + ] + } + }, + { + "names": [ + "get_mempolicy", + "mbind", + "set_mempolicy", + "set_mempolicy_home_node" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_SYS_NICE" + ] + } + }, + { + "names": [ + "syslog" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_SYSLOG" + ] + } + }, + { + "names": [ + "bpf" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_BPF" + ] + } + }, + { + "names": [ + "perf_event_open" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": [ + "CAP_PERFMON" + ] + } + } + ] +} diff --git a/php/domain-validator.php b/php/domain-validator.php new file mode 100644 index 00000000..57506b8a --- /dev/null +++ b/php/domain-validator.php @@ -0,0 +1,19 @@ + - - - - $args - $args - $args - $args - $request - $request - $request - $response - $response - $response - - - - - $args - - - - - $args - $args - $args - $args - $args - $args - $args - $args - - - $request->getParsedBody()['selected_restore_time'] - - - $request->getParsedBody()['selected_restore_time'] - - - - - $args - $args - $args - - - $request->getParsedBody()['password'] - - - $password - - - $request->getParsedBody()['password'] - - - - - IContainerState - - - $internalPort - - - $container->GetInternalPorts() !== null - - - - - withStatus - - - - - $object - - - + diff --git a/php/psalm.xml b/php/psalm.xml index 49c40fd4..576d82d2 100644 --- a/php/psalm.xml +++ b/php/psalm.xml @@ -1,15 +1,29 @@ + + + + + + + + + + + + + + diff --git a/php/public/automatic_reload.js b/php/public/automatic_reload.js index c8201c64..7b14a3c4 100644 --- a/php/public/automatic_reload.js +++ b/php/public/automatic_reload.js @@ -1,17 +1,19 @@ -if (document.hasFocus()) { - // hide reload button if the site reloads automatically - var list = document.getElementsByClassName("reload button"); - for (var i = 0; i < list.length; i++) { - // list[i] is a node with the desired class name - list[i].style.display = 'none'; - } +document.addEventListener("DOMContentLoaded", function(event) { + if (document.hasFocus()) { + // hide reload button if the site reloads automatically + let list = document.getElementsByClassName("reload button"); + for (let i = 0; i < list.length; i++) { + // list[i] is a node with the desired class name + list[i].style.display = 'none'; + } - // set timeout for reload - setTimeout(function(){ - window.location.reload(1); - }, 5000); -} else { - window.addEventListener("beforeunload", function() { - document.getElementById('overlay').classList.add('loading') - }); -} \ No newline at end of file + // set timeout for reload + setTimeout(function(){ + window.location.reload(1); + }, 5000); + } else { + window.addEventListener("beforeunload", function() { + document.getElementById('overlay').classList.add('loading') + }); + } +}); diff --git a/php/public/base_path.js b/php/public/base_path.js new file mode 100644 index 00000000..a55ed943 --- /dev/null +++ b/php/public/base_path.js @@ -0,0 +1,7 @@ +document.addEventListener("DOMContentLoaded", function() { + basePath = document.getElementById("base_path") + if (basePath) { + // Remove '/containers' from the end of the path, to get the base path only + basePath.value = window.location.pathname.slice(0, -11); + } +}); \ No newline at end of file diff --git a/php/public/containers-form-submit.js b/php/public/containers-form-submit.js new file mode 100644 index 00000000..1382bced --- /dev/null +++ b/php/public/containers-form-submit.js @@ -0,0 +1,140 @@ +document.addEventListener("DOMContentLoaded", function () { + // Hide submit button initially + const optionsFormSubmit = document.querySelectorAll(".options-form-submit"); + optionsFormSubmit.forEach(element => { + element.style.display = 'none'; + }); + + const communityFormSubmit = document.getElementById("community-form-submit"); + communityFormSubmit.style.display = 'none'; + + // Store initial states for all checkboxes + const initialStateOptionsContainers = {}; + const initialStateCommunityContainers = {}; + const optionsContainersCheckboxes = document.querySelectorAll("#options-form input[type='checkbox']"); + const communityContainersCheckboxes = document.querySelectorAll("#community-form input[type='checkbox']"); + + // Office suite radio buttons + const collaboraRadio = document.getElementById('office-collabora'); + const onlyofficeRadio = document.getElementById('office-onlyoffice'); + const noneRadio = document.getElementById('office-none'); + const collaboraHidden = document.getElementById('collabora'); + const onlyofficeHidden = document.getElementById('onlyoffice'); + let initialOfficeSelection = null; + + optionsContainersCheckboxes.forEach(checkbox => { + initialStateOptionsContainers[checkbox.id] = checkbox.checked; // Use checked property to capture actual initial state + }); + + communityContainersCheckboxes.forEach(checkbox => { + initialStateCommunityContainers[checkbox.id] = checkbox.checked; // Use checked property to capture actual initial state + }); + + // Store initial office suite selection + if (collaboraRadio && onlyofficeRadio && noneRadio) { + if (collaboraRadio.checked) { + initialOfficeSelection = 'collabora'; + } else if (onlyofficeRadio.checked) { + initialOfficeSelection = 'onlyoffice'; + } else { + initialOfficeSelection = 'none'; + } + } + + // Function to compare current states to initial states + function checkForOptionContainerChanges() { + let hasChanges = false; + + optionsContainersCheckboxes.forEach(checkbox => { + if (checkbox.checked !== initialStateOptionsContainers[checkbox.id]) { + hasChanges = true; + } + }); + + // Check office suite changes and sync to hidden inputs + if (collaboraRadio && onlyofficeRadio && noneRadio && collaboraHidden && onlyofficeHidden) { + let currentOfficeSelection = null; + if (collaboraRadio.checked) { + currentOfficeSelection = 'collabora'; + collaboraHidden.value = 'on'; + onlyofficeHidden.value = ''; + } else if (onlyofficeRadio.checked) { + currentOfficeSelection = 'onlyoffice'; + collaboraHidden.value = ''; + onlyofficeHidden.value = 'on'; + } else { + currentOfficeSelection = 'none'; + collaboraHidden.value = ''; + onlyofficeHidden.value = ''; + } + + if (currentOfficeSelection !== initialOfficeSelection) { + hasChanges = true; + } + } + + // Show or hide submit button based on changes + optionsFormSubmit.forEach(element => { + element.style.display = hasChanges ? 'block' : 'none'; + }); + } + + // Function to compare current states to initial states + function checkForCommunityContainerChanges() { + let hasChanges = false; + + communityContainersCheckboxes.forEach(checkbox => { + if (checkbox.checked !== initialStateCommunityContainers[checkbox.id]) { + hasChanges = true; + } + }); + + // Show or hide submit button based on changes + communityFormSubmit.style.display = hasChanges ? 'block' : 'none'; + } + + // Event listener to trigger visibility check on each change + optionsContainersCheckboxes.forEach(checkbox => { + checkbox.addEventListener("change", checkForOptionContainerChanges); + }); + + communityContainersCheckboxes.forEach(checkbox => { + checkbox.addEventListener("change", checkForCommunityContainerChanges); + }); + + // Custom behaviors for specific options + function handleTalkVisibility() { + const talkRecording = document.getElementById("talk-recording"); + if (document.getElementById("talk").checked) { + talkRecording.disabled = false; + } else { + talkRecording.checked = false; + talkRecording.disabled = true; + } + checkForOptionContainerChanges(); // Check changes after toggling Talk Recording + } + + function handleDockerSocketProxyWarning() { + if (document.getElementById("docker-socket-proxy").checked) { + alert('⚠️ Warning! Enabling this container comes with possible Security problems since you are exposing the docker socket and all its privileges to the Nextcloud container. Enable this only if you are sure what you are doing!'); + } + } + + // Initialize event listeners for specific behaviors + document.getElementById("talk").addEventListener('change', handleTalkVisibility); + document.getElementById("docker-socket-proxy").addEventListener('change', handleDockerSocketProxyWarning); + + // Initialize talk-recording visibility on page load + handleTalkVisibility(); // Ensure talk-recording is correctly initialized + + // Add event listeners for office suite radio buttons + if (collaboraRadio && onlyofficeRadio && noneRadio) { + collaboraRadio.addEventListener('change', checkForOptionContainerChanges); + onlyofficeRadio.addEventListener('change', checkForOptionContainerChanges); + noneRadio.addEventListener('change', checkForOptionContainerChanges); + } + + // Initial call to check for changes + checkForOptionContainerChanges(); + checkForCommunityContainerChanges(); +}); diff --git a/php/public/disable-clamav.js b/php/public/disable-clamav.js index ae2d003c..18b08081 100644 --- a/php/public/disable-clamav.js +++ b/php/public/disable-clamav.js @@ -1,5 +1,5 @@ document.addEventListener("DOMContentLoaded", function(event) { // Clamav - var clamav = document.getElementById("clamav"); + let clamav = document.getElementById("clamav"); clamav.disabled = true; }); \ No newline at end of file diff --git a/php/public/disable-collabora.js b/php/public/disable-collabora.js index d6a4ce02..762252ce 100644 --- a/php/public/disable-collabora.js +++ b/php/public/disable-collabora.js @@ -1,5 +1,5 @@ document.addEventListener("DOMContentLoaded", function(event) { // Collabora - var collabora = document.getElementById("collabora"); + const collabora = document.getElementById("office-collabora"); collabora.disabled = true; }); \ No newline at end of file diff --git a/php/public/disable-docker-socket-proxy.js b/php/public/disable-docker-socket-proxy.js new file mode 100644 index 00000000..7491042e --- /dev/null +++ b/php/public/disable-docker-socket-proxy.js @@ -0,0 +1,7 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Docker socket proxy + let dockerSocketProxy = document.getElementById("docker-socket-proxy"); + if (dockerSocketProxy) { + dockerSocketProxy.disabled = true; + } +}); diff --git a/php/public/disable-fulltextsearch.js b/php/public/disable-fulltextsearch.js index de614bf5..d3ca21fe 100644 --- a/php/public/disable-fulltextsearch.js +++ b/php/public/disable-fulltextsearch.js @@ -1,5 +1,5 @@ document.addEventListener("DOMContentLoaded", function(event) { // Fulltextsearch - var fulltextsearch = document.getElementById("fulltextsearch"); + let fulltextsearch = document.getElementById("fulltextsearch"); fulltextsearch.disabled = true; }); \ No newline at end of file diff --git a/php/public/disable-imaginary.js b/php/public/disable-imaginary.js index 0f7787c9..9cdec5fa 100644 --- a/php/public/disable-imaginary.js +++ b/php/public/disable-imaginary.js @@ -1,5 +1,5 @@ document.addEventListener("DOMContentLoaded", function(event) { // Imaginary - var imaginary = document.getElementById("imaginary"); + let imaginary = document.getElementById("imaginary"); imaginary.disabled = true; }); \ No newline at end of file diff --git a/php/public/disable-onlyoffice.js b/php/public/disable-onlyoffice.js index b4d30dec..c660bd9d 100644 --- a/php/public/disable-onlyoffice.js +++ b/php/public/disable-onlyoffice.js @@ -1,9 +1,5 @@ document.addEventListener("DOMContentLoaded", function(event) { // OnlyOffice - try { - var onlyoffice = document.getElementById("onlyoffice"); - onlyoffice.disabled = true; - } catch (error) { - // console.error(error); - } + const onlyoffice = document.getElementById("office-onlyoffice"); + onlyoffice.disabled = true; }); \ No newline at end of file diff --git a/php/public/disable-talk-recording.js b/php/public/disable-talk-recording.js new file mode 100644 index 00000000..72c5de32 --- /dev/null +++ b/php/public/disable-talk-recording.js @@ -0,0 +1,4 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Talk-recording + document.getElementById("talk-recording").disabled = true; +}); diff --git a/php/public/disable-talk.js b/php/public/disable-talk.js index 7b4a8719..c37d72af 100644 --- a/php/public/disable-talk.js +++ b/php/public/disable-talk.js @@ -1,5 +1,5 @@ document.addEventListener("DOMContentLoaded", function(event) { // Talk - var talk = document.getElementById("talk"); + let talk = document.getElementById("talk"); talk.disabled = true; }); \ No newline at end of file diff --git a/php/public/disable-whiteboard.js b/php/public/disable-whiteboard.js new file mode 100644 index 00000000..50e1215d --- /dev/null +++ b/php/public/disable-whiteboard.js @@ -0,0 +1,5 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Whiteboard + let whiteboard = document.getElementById("whiteboard"); + whiteboard.disabled = true; +}); diff --git a/php/public/forms.js b/php/public/forms.js index 4202f31e..3adc3997 100644 --- a/php/public/forms.js +++ b/php/public/forms.js @@ -1,6 +1,16 @@ "use strict"; + +function showPassword(id) { + let passwordField = document.getElementById(id); + if (passwordField.type === "password" && passwordField.value !== "") { + passwordField.type = "text"; + } else if (passwordField.type === "text" && passwordField.value === "") { + passwordField.type = "password"; + } +} + (function (){ - var lastError; + let lastError; function showError(message) { const body = document.getElementsByTagName('body')[0] @@ -19,25 +29,27 @@ const xhr = e.target; if (xhr.status === 201) { window.location.replace(xhr.getResponseHeader('Location')); - } - if (xhr.status === 422) { + } else if (xhr.status === 422) { + disableSpinner() showError(xhr.response); - } - if (xhr.status === 500) { - showError("Server error. Please see the logs for details."); + } else if (xhr.status === 500) { + showError("Server error. Please check the mastercontainer logs for details. This page will reload after 10s automatically. Then you can check the mastercontainer logs."); + // Reload after 10s since it is expected that the updated view is shown (e.g. after starting containers) + setTimeout(function(){ + window.location.reload(1); + }, 10000); + } else { + // If the responose is not one of the above, we should reload to show the latest content + window.location.reload(1); } } - function disable(element) { + function enableSpinner() { document.getElementById('overlay').classList.add('loading'); - element.classList.add('loading'); - element.disabled = true; } - function enable(element) { + function disableSpinner() { document.getElementById('overlay').classList.remove('loading'); - element.classList.remove('loading'); - element.disabled = false; } function initForm(form) { @@ -46,14 +58,13 @@ if (lastError) { lastError.remove() } - var xhr = new XMLHttpRequest(); + let xhr = new XMLHttpRequest(); xhr.addEventListener('load', handleEvent); xhr.addEventListener('error', () => showError("Failed to talk to server.")); - xhr.addEventListener('load', () => enable(event.submitter)); - xhr.addEventListener('error', () => enable(event.submitter)); + xhr.addEventListener('error', () => disableSpinner()); xhr.open(form.method, form.getAttribute("action")); xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - disable(event.submitter); + enableSpinner(); xhr.send(new URLSearchParams(new FormData(form))); event.preventDefault(); } diff --git a/php/public/img/background.png b/php/public/img/background.png deleted file mode 100644 index 1d4d5e8c..00000000 Binary files a/php/public/img/background.png and /dev/null differ diff --git a/php/public/img/jo-myoung-hee-fluid-dark.webp b/php/public/img/jo-myoung-hee-fluid-dark.webp new file mode 100644 index 00000000..314048f9 Binary files /dev/null and b/php/public/img/jo-myoung-hee-fluid-dark.webp differ diff --git a/php/public/img/jo-myoung-hee-fluid.webp b/php/public/img/jo-myoung-hee-fluid.webp new file mode 100644 index 00000000..1e9f4aad Binary files /dev/null and b/php/public/img/jo-myoung-hee-fluid.webp differ diff --git a/php/public/img/logo-blue.svg b/php/public/img/logo-blue.svg deleted file mode 100644 index cc0cdb65..00000000 --- a/php/public/img/logo-blue.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -image/svg+xml \ No newline at end of file diff --git a/php/public/img/logo.svg b/php/public/img/logo.svg deleted file mode 100644 index 0b2aef61..00000000 --- a/php/public/img/logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/php/public/img/nextcloud-logo.svg b/php/public/img/nextcloud-logo.svg new file mode 100644 index 00000000..94b07449 --- /dev/null +++ b/php/public/img/nextcloud-logo.svg @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/php/public/index.php b/php/public/index.php index 3829017a..cc06bb90 100644 --- a/php/public/index.php +++ b/php/public/index.php @@ -7,11 +7,16 @@ ini_set('memory_limit', '2048M'); // set max execution time to 2h just in case of a very slow internet connection ini_set('max_execution_time', '7200'); +// Log whole log messages +ini_set('log_errors_max_len', '0'); + use DI\Container; use Slim\Csrf\Guard; use Slim\Factory\AppFactory; use Slim\Views\Twig; use Slim\Views\TwigMiddleware; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; require __DIR__ . '/../vendor/autoload.php'; @@ -22,6 +27,9 @@ ini_set('session.save_path', $dataConst->GetSessionDirectory()); // Auto logout on browser close ini_set('session.cookie_lifetime', '0'); +# Keep session for 24h max +ini_set('session.gc_maxlifetime', '86400'); + // Create app AppFactory::setContainer($container); $app = AppFactory::create(); @@ -52,6 +60,8 @@ $app->get('/api/docker/getwatchtower', AIO\Controller\DockerController::class . $app->post('/api/docker/start', AIO\Controller\DockerController::class . ':StartContainer'); $app->post('/api/docker/backup', AIO\Controller\DockerController::class . ':StartBackupContainerBackup'); $app->post('/api/docker/backup-check', AIO\Controller\DockerController::class . ':StartBackupContainerCheck'); +$app->post('/api/docker/backup-list', AIO\Controller\DockerController::class . ':StartBackupContainerList'); +$app->post('/api/docker/backup-check-repair', AIO\Controller\DockerController::class . ':StartBackupContainerCheckRepair'); $app->post('/api/docker/backup-test', AIO\Controller\DockerController::class . ':StartBackupContainerTest'); $app->post('/api/docker/restore', AIO\Controller\DockerController::class . ':StartBackupContainerRestore'); $app->post('/api/docker/stop', AIO\Controller\DockerController::class . ':StopContainer'); @@ -62,60 +72,85 @@ $app->post('/api/auth/logout', AIO\Controller\LoginController::class . ':Logout' $app->post('/api/configuration', \AIO\Controller\ConfigurationController::class . ':SetConfig'); // Views -$app->get('/containers', function ($request, $response, $args) use ($container) { +$app->get('/containers', function (Request $request, Response $response, array $args) use ($container) { $view = Twig::fromRequest($request); + $view->addExtension(new \AIO\Twig\ClassExtension()); /** @var \AIO\Data\ConfigurationManager $configurationManager */ $configurationManager = $container->get(\AIO\Data\ConfigurationManager::class); - $dockerActionManger = $container->get(\AIO\Docker\DockerActionManager::class); - $dockerActionManger->ConnectMasterContainerToNetwork(); + /** @var \AIO\Docker\DockerActionManager $dockerActionManager */ + $dockerActionManager = $container->get(\AIO\Docker\DockerActionManager::class); + /** @var \AIO\Controller\DockerController $dockerController */ $dockerController = $container->get(\AIO\Controller\DockerController::class); + $dockerActionManager->ConnectMasterContainerToNetwork(); $dockerController->StartDomaincheckContainer(); - $view->addExtension(new \AIO\Twig\ClassExtension()); + + // Check if bypass_mastercontainer_update is provided on the URL, a special developer mode to bypass a mastercontainer update and use local image. + $params = $request->getQueryParams(); + $bypass_mastercontainer_update = isset($params['bypass_mastercontainer_update']); + $bypass_container_update = isset($params['bypass_container_update']); + $skip_domain_validation = isset($params['skip_domain_validation']); + return $view->render($response, 'containers.twig', [ - 'domain' => $configurationManager->GetDomain(), - 'borg_backup_host_location' => $configurationManager->GetBorgBackupHostLocation(), - 'nextcloud_password' => $configurationManager->GetSecret('NEXTCLOUD_PASSWORD'), + 'domain' => $configurationManager->domain, + 'apache_port' => $configurationManager->apachePort, + 'borg_backup_host_location' => $configurationManager->borgBackupHostLocation, + 'borg_remote_repo' => $configurationManager->borgRemoteRepo, + 'borg_public_key' => $configurationManager->getBorgPublicKey(), + 'nextcloud_password' => $configurationManager->getAndGenerateSecret('NEXTCLOUD_PASSWORD'), 'containers' => (new \AIO\ContainerDefinitionFetcher($container->get(\AIO\Data\ConfigurationManager::class), $container))->FetchDefinition(), - 'borgbackup_password' => $configurationManager->GetSecret('BORGBACKUP_PASSWORD'), - 'is_mastercontainer_update_available' => $dockerActionManger->IsMastercontainerUpdateAvailable(), + 'borgbackup_password' => $configurationManager->getAndGenerateSecret('BORGBACKUP_PASSWORD'), + 'is_mastercontainer_update_available' => ( $bypass_mastercontainer_update ? false : $dockerActionManager->IsMastercontainerUpdateAvailable() ), 'has_backup_run_once' => $configurationManager->hasBackupRunOnce(), - 'is_backup_container_running' => $dockerActionManger->isBackupContainerRunning(), - 'backup_exit_code' => $dockerActionManger->GetBackupcontainerExitCode(), - 'is_instance_restore_attempt' => $configurationManager->isInstanceRestoreAttempt(), - 'borg_backup_mode' => $configurationManager->GetBorgBackupMode(), - 'was_start_button_clicked' => $configurationManager->wasStartButtonClicked(), - 'has_update_available' => $dockerActionManger->isAnyUpdateAvailable(), - 'last_backup_time' => $configurationManager->GetLastBackupTime(), - 'backup_times' => $configurationManager->GetBackupTimes(), - 'current_channel' => $dockerActionManger->GetCurrentChannel(), - 'is_x64_platform' => $configurationManager->isx64Platform(), - 'is_clamav_enabled' => $configurationManager->isClamavEnabled(), - 'is_onlyoffice_enabled' => $configurationManager->isOnlyofficeEnabled(), - 'is_collabora_enabled' => $configurationManager->isCollaboraEnabled(), - 'is_talk_enabled' => $configurationManager->isTalkEnabled(), - 'borg_restore_password' => $configurationManager->GetBorgRestorePassword(), - 'daily_backup_time' => $configurationManager->GetDailyBackupTime(), + 'is_backup_container_running' => $dockerActionManager->isBackupContainerRunning(), + 'backup_exit_code' => $dockerActionManager->GetBackupcontainerExitCode(), + 'is_instance_restore_attempt' => $configurationManager->instanceRestoreAttempt, + 'borg_backup_mode' => $configurationManager->backupMode, + 'was_start_button_clicked' => $configurationManager->wasStartButtonClicked, + 'has_update_available' => $dockerActionManager->isAnyUpdateAvailable(), + 'last_backup_time' => $configurationManager->getLastBackupTime(), + 'backup_times' => $configurationManager->getBackupTimes(), + 'current_channel' => $dockerActionManager->GetCurrentChannel(), + 'is_clamav_enabled' => $configurationManager->isClamavEnabled, + 'is_onlyoffice_enabled' => $configurationManager->isOnlyofficeEnabled, + 'is_collabora_enabled' => $configurationManager->isCollaboraEnabled, + 'is_talk_enabled' => $configurationManager->isTalkEnabled, + 'borg_restore_password' => $configurationManager->borgRestorePassword, + 'daily_backup_time' => $configurationManager->getDailyBackupTime(), 'is_daily_backup_running' => $configurationManager->isDailyBackupRunning(), - 'timezone' => $configurationManager->GetTimezone(), - 'skip_domain_validation' => $configurationManager->shouldDomainValidationBeSkipped(), - 'talk_port' => $configurationManager->GetTalkPort(), - 'collabora_dictionaries' => $configurationManager->GetCollaboraDictionaries(), + 'timezone' => $configurationManager->timezone, + 'skip_domain_validation' => $configurationManager->shouldDomainValidationBeSkipped($skip_domain_validation), + 'talk_port' => $configurationManager->talkPort, + 'collabora_dictionaries' => $configurationManager->collaboraDictionaries, + 'collabora_additional_options' => $configurationManager->collaboraAdditionalOptions, 'automatic_updates' => $configurationManager->areAutomaticUpdatesEnabled(), - 'is_backup_section_enabled' => $configurationManager->isBackupSectionEnabled(), - 'is_imaginary_enabled' => $configurationManager->isImaginaryEnabled(), - 'is_fulltextsearch_enabled' => $configurationManager->isFulltextsearchEnabled(), - 'additional_backup_directories' => $configurationManager->GetAdditionalBackupDirectoriesString(), + 'is_backup_section_enabled' => !$configurationManager->disableBackupSection, + 'is_imaginary_enabled' => $configurationManager->isImaginaryEnabled, + 'is_fulltextsearch_enabled' => $configurationManager->isFulltextsearchEnabled, + 'additional_backup_directories' => $configurationManager->getAdditionalBackupDirectoriesString(), + 'nextcloud_datadir' => $configurationManager->nextcloudDatadirMount, + 'nextcloud_mount' => $configurationManager->nextcloudMount, + 'nextcloud_upload_limit' => $configurationManager->nextcloudUploadLimit, + 'nextcloud_max_time' => $configurationManager->nextcloudMaxTime, + 'nextcloud_memory_limit' => $configurationManager->nextcloudMemoryLimit, + 'is_dri_device_enabled' => $configurationManager->nextcloudEnableDriDevice, + 'is_nvidia_gpu_enabled' => $configurationManager->enableNvidiaGpu, + 'is_talk_recording_enabled' => $configurationManager->isTalkRecordingEnabled, + 'is_docker_socket_proxy_enabled' => $configurationManager->isDockerSocketProxyEnabled, + 'is_whiteboard_enabled' => $configurationManager->isWhiteboardEnabled, + 'community_containers' => $configurationManager->listAvailableCommunityContainers(), + 'community_containers_enabled' => $configurationManager->aioCommunityContainers, + 'bypass_container_update' => $bypass_container_update, ]); })->setName('profile'); -$app->get('/login', function ($request, $response, $args) use ($container) { +$app->get('/login', function (Request $request, Response $response, array $args) use ($container) { $view = Twig::fromRequest($request); - /** @var \AIO\Docker\DockerActionManager $dockerActionManger */ - $dockerActionManger = $container->get(\AIO\Docker\DockerActionManager::class); + /** @var \AIO\Docker\DockerActionManager $dockerActionManager */ + $dockerActionManager = $container->get(\AIO\Docker\DockerActionManager::class); return $view->render($response, 'login.twig', [ - 'is_login_allowed' => $dockerActionManger->isLoginAllowed(), + 'is_login_allowed' => $dockerActionManager->isLoginAllowed(), ]); }); -$app->get('/setup', function ($request, $response, $args) use ($container) { +$app->get('/setup', function (Request $request, Response $response, array $args) use ($container) { $view = Twig::fromRequest($request); /** @var \AIO\Data\Setup $setup */ $setup = $container->get(\AIO\Data\Setup::class); @@ -137,28 +172,29 @@ $app->get('/setup', function ($request, $response, $args) use ($container) { }); // Auth Redirector -$app->get('/', function (\Psr\Http\Message\RequestInterface $request, \Psr\Http\Message\ResponseInterface $response, $args) use ($container) { +$app->get('/', function (\Psr\Http\Message\RequestInterface $request, Response $response, array $args) use ($container) { + /** @var \AIO\Auth\AuthManager $authManager */ $authManager = $container->get(\AIO\Auth\AuthManager::class); /** @var \AIO\Data\Setup $setup */ $setup = $container->get(\AIO\Data\Setup::class); if($setup->CanBeInstalled()) { return $response - ->withHeader('Location', '/setup') + ->withHeader('Location', 'setup') ->withStatus(302); } if($authManager->IsAuthenticated()) { return $response - ->withHeader('Location', '/containers') + ->withHeader('Location', 'containers') ->withStatus(302); } else { return $response - ->withHeader('Location', '/login') + ->withHeader('Location', 'login') ->withStatus(302); } }); -$errorMiddleware = $app->addErrorMiddleware(true, true, true); +$errorMiddleware = $app->addErrorMiddleware(false, true, true); $app->run(); diff --git a/php/public/options-form-submit.js b/php/public/options-form-submit.js deleted file mode 100644 index e602f177..00000000 --- a/php/public/options-form-submit.js +++ /dev/null @@ -1,38 +0,0 @@ -function makeOptionsFormSubmitVisible() { - var optionsFormSubmit = document.getElementById("options-form-submit"); - optionsFormSubmit.style.display = 'block'; -} - -document.addEventListener("DOMContentLoaded", function(event) { - // handle submit button for options form - var optionsFormSubmit = document.getElementById("options-form-submit"); - optionsFormSubmit.style.display = 'none'; - - // Clamav - var clamav = document.getElementById("clamav"); - clamav.addEventListener('change', makeOptionsFormSubmitVisible); - - // OnlyOffice - try { - var onlyoffice = document.getElementById("onlyoffice"); - onlyoffice.addEventListener('change', makeOptionsFormSubmitVisible); - } catch (error) { - // console.error(error); - } - - // Collabora - var collabora = document.getElementById("collabora"); - collabora.addEventListener('change', makeOptionsFormSubmitVisible); - - // Talk - var talk = document.getElementById("talk"); - talk.addEventListener('change', makeOptionsFormSubmitVisible); - - // Imaginary - var imaginary = document.getElementById("imaginary"); - imaginary.addEventListener('change', makeOptionsFormSubmitVisible); - - // Fulltextsearch - var fulltextsearch = document.getElementById("fulltextsearch"); - fulltextsearch.addEventListener('change', makeOptionsFormSubmitVisible); -}); diff --git a/php/public/second-tab-warning.js b/php/public/second-tab-warning.js new file mode 100644 index 00000000..63abc18b --- /dev/null +++ b/php/public/second-tab-warning.js @@ -0,0 +1,12 @@ +const channel = new BroadcastChannel('tab') + +channel.postMessage('second-tab') +// note that listener is added after posting the message + +channel.addEventListener('message', (msg) => { + if (msg.data === 'second-tab') { + // message received from 2nd tab + document.getElementById('overlay').classList.add('loading') + alert('Cannot open multiple instances. You can use AIO here by reloading the page.') + } +}); \ No newline at end of file diff --git a/php/public/style.css b/php/public/style.css index 3c619b24..b35883d0 100644 --- a/php/public/style.css +++ b/php/public/style.css @@ -1,28 +1,140 @@ +:root { + --color-nextcloud-blue: #0082c9; + --color-nextcloud-logo: var(--color-nextcloud-blue); + --color-main-background: white; + --color-input-background: white; + --color-main-text: black; + --color-main-border: black; + --color-main-border-hover: var(--color-main-border); + --color-error: #db0606; + --color-error-hover: #df2525; + --color-error-text: #c20505; + --color-success: #46ba61; + --color-running: #ffd000; + --color-primary-element: #00679e; + --color-primary-element-hover: #005a8a; + --color-primary-element-text: #ffffff; + --color-primary-element-light: #e5eff5; + --color-primary-element-light-hover: #dbe4ea; + --color-primary-element-light-text: #00293f; + --color-border-maxcontrast: #7d7d7d; + --color-loader: #f3f3f3; + --color-disabled: #d3d3d3; /* light gray background for disabled checkboxes */ + --color-border-disabled: #a9a9a9; /* darker gray border for disabled checkboxes */ + --color-text-disabled: #a9a9a9; /* matching label text color for disabled checkboxes */ + --border: .5px; + --border-hover: 2px; + --border-radius: 7px; + --border-radius-large: 12px; + --default-font-size: 13px; + --checkbox-size: 16px; + --max-width: 580px; + --container-top-margin: 20px; + --container-bottom-margin: 20px; + --container-padding: 2px; + --container-height-calculation-difference: calc(var(--container-top-margin) + var(--container-bottom-margin)); + --main-height-calculation-difference: calc(var(--container-height-calculation-difference) + calc(var(--container-padding) * 2)); + --main-padding: 50px; +} + +/* Breakpoint calculation: 580px (max-width) + 100px (main-padding * 2) + 200px (additional space) = 880px +Note: Unfortunately, it's not possible to calculate this dynamically using CSS variables in media queries */ +@media only screen and (max-width: 880px) { + :root { + --container-top-margin: 50px; + --container-bottom-margin: 0px; + } +} + +[data-theme="dark"] { + --color-main-background: #171717; + --color-input-background: #ebebeb; + --color-main-text: #ebebeb; + --color-nextcloud-logo: var(--color-main-text); + --color-main-border: var(--color-border-maxcontrast); + --color-main-border-hover: var(--color-main-text); + --color-error: #ff3333; + --color-error-hover: #ff6666; + --color-error-text: #ff8080; + --color-primary-element:#0091f2; + --color-primary-element-hover:#079cff; + --color-primary-element-text:#000000; + --color-primary-element-light:#14232c; + --color-primary-element-light-hover:#1e2d35; + --color-primary-element-light-text:#99d3f9; + --color-loader: var(--color-border-maxcontrast); + --border-hover: var(--border); +} + html, body { padding: 0; margin: 0; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Cantarell, Ubuntu, Helvetica Neue, Arial, Noto Color Emoji, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;; + font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + background-color: var(--color-main-background); + color: var(--color-main-text); } a { text-decoration: none; + color: var(--color-primary-element); } -.button { - padding: 6px 16px; +a:hover { + color: var(--color-primary-element-hover); +} + +a.button, +input[type="submit"] { + padding: 8px 16px; width: auto; - min-height: 34px; + height: 34px; cursor: pointer; - background-color:#0082c9; + background-color: var(--color-primary-element); font-weight: bold; - border-radius: 100px; + border-radius: var(--border-radius); margin: 3px 3px 3px 0; - font-size: 13px; - color: white; - border: 1px solid black; + font-size: var(--default-font-size); + color: var(--color-primary-element-text); + border: none; outline: none; } +a.button:focus, +input[type="submit"]:focus { + outline: 2px solid var(--color-main-border); +} + +a.button:hover, +input[type="submit"]:hover { + background-color: var(--color-primary-element-hover); +} + + +a.button.light:hover, +input[type="submit"].light:hover { + background-color: var(--color-primary-element-light); + color: var(--color-primary-element-light-text); +} + + +a.button.light, +input[type="submit"].light { + background-color: var(--color-primary-element-light); +} + +a.button.error, +input[type="submit"].error { + background-color: var(--color-error); +} + +a.button.error:hover, +input[type="submit"].error:hover { + background-color: var(--color-error-hover); +} + + + + summary { cursor: pointer; } @@ -34,38 +146,36 @@ ul { li { padding-bottom: 5px; + text-indent: 0; + padding-left: 0; } span.error { - background-color: #e9322d; + background-color: var(--color-error); } div.toast.error { - border-left-color: #e9322d; + border-left-color: var(--color-error); } .status { display: inline-block; - height: 16px; - width: 16px; - vertical-align: text-bottom -} - -.status { + height: var(--checkbox-size); + width: var(--checkbox-size); + vertical-align: text-bottom; border-radius: 50% } - span.success { - background-color: #46ba61; + background-color: var(--color-success); } span.running { - background-color: rgb(255, 208, 0); + background-color: var(--color-running); } div.toast.success { - border-left-color: #46ba61; + border-left-color: var(--color-success); } div.toast { @@ -76,34 +186,47 @@ div.toast { padding: 12px; margin-top: 45px; position: fixed; - z-index: 1; - border-radius: 3px; - background: none; - background-color: white; + z-index: 1000; + border-radius: var(--border-radius); + background: var(--color-main-background) none; + color: var(--color-main-text); +} + +.nextcloud-logo { + margin-left: auto; + margin-right: auto; + display: block; + color: var(--color-nextcloud-logo); +} + +.fallback-text { + display: none; +} + +svg:not(:has(use)) .fallback-text { + display: block; } .login { padding: 50px; - background-color: white; + background-color: var(--color-main-background); + color: var(--color-main-text); width: 500px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); - border-radius: 16px; + border-radius: var(--border-radius-large); } .login > .monospace { - font-family: monospace; + font-family: monospace, monospace, system-ui, -apple-system, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; font-size: 17px; } -input { - padding: 10px; - margin-bottom: 15px; -} - -.login > form > input { +.login > form > input[type="password"], +.login > form > input[type="text"], +.login > form > input[type="submit"] { width: 100%; } @@ -113,44 +236,125 @@ input { display: block; } -.login > .button { +.login a.button, +.login input[type="submit"] { margin-left: auto; margin-right: auto; display: block; text-align: center; - line-height: 33px; - margin-top: 20px; + padding: 0px; + align-content: center; } -.login-wrapper { - height: 100%; - width: 100%; - background-color: #0082c9; - background-image: linear-gradient( - 40deg - , #0082c9 0%, #30b6ff 100%); - background-size: contain; - background-image: url('/img/background.png'), linear-gradient( - 40deg - , #0082c9 0%, #30b6ff 100%); - position: relative; +.wrapper { + min-height: 100dvh; + min-width: 100vw; + position: fixed; + width: 100vw; + background-image: url("img/jo-myoung-hee-fluid.webp"); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + box-sizing: border-box; + overflow: hidden; } -.content { - padding: 20px; +html[data-theme="dark"] .wrapper { + background-image: url("img/jo-myoung-hee-fluid-dark.webp"); +} + +form { + margin: 0; +} + +input[type="text"], +input[type="password"], +select { + padding-left: 8px; + padding-right: 8px; + height: 34px; + margin-bottom: 15px; + border-radius: var(--border-radius); + border: var(--border) solid var(--color-border-maxcontrast); + background: var(--color-main-background); + color: var(--color-main-text); +} + +input[type="text"]:hover, +input[type="password"]:hover, +select:hover { + border: var(--border-hover) solid var(--color-main-border-hover); +} + +textarea { + border-radius: var(--border-radius); + border: .5px solid var(--color-main-border); max-width: 100%; +} + +input[type="text"]:focus, +input[type="password"]:focus, +textarea:focus, +select:focus { + border: 1px solid var(--color-main-border); +} + +/* Scroll bar for dark mode */ +html[data-theme="dark"] ::-webkit-scrollbar { + width: 8px; /* Width of the scroll bar */ +} + +html[data-theme="dark"] ::-webkit-scrollbar-thumb { + background-color: #444; /* Dark mode scrollbar thumb color */ + border-radius: 4px; /* Rounded corners for the thumb */ +} + +html[data-theme="dark"] ::-webkit-scrollbar-track { + background-color: #333; /* Dark mode scrollbar track color */ +} + +/* Scroll bar for light mode */ +::-webkit-scrollbar { + width: 8px; /* Width of the scroll bar */ +} + +::-webkit-scrollbar-thumb { + background-color: #888; /* Light mode scrollbar thumb color */ + border-radius: 4px; /* Rounded corners for the thumb */ +} + +::-webkit-scrollbar-track { + background-color: #f0f0f0; /* Light mode scrollbar track color */ +} + +.container { + margin: var(--container-top-margin) auto var(--container-bottom-margin) auto; + padding: var(--container-padding); + max-width: calc(var(--max-width) + calc(var(--main-padding) * 2) + 8px); + background-color: var(--color-main-background); + border-radius: var(--border-radius-large); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + max-height: calc(100dvh - var(--container-height-calculation-difference)); + overflow: hidden; +} + +main { + padding-left: var(--main-padding); + padding-right: var(--main-padding); + background-color: transparent; /* transparent, since color comes from outer container */ + color: var(--color-main-text); + max-height: calc(100dvh - var(--main-height-calculation-difference)); + overflow-y: auto; + box-sizing: border-box; word-break: break-word; - max-width: 470px; + max-width: calc(var(--max-width) + calc(var(--main-padding) * 2)); margin: 0 auto; + padding-bottom: var(--main-padding); } .logo { - background-image: url('/img/logo.svg'); + color: white; height: 50px; - background-repeat: no-repeat; - display: inline-flex; - background-size: contain; - background-position: center center; width: 62px; position: absolute; left: 12px; @@ -159,11 +363,93 @@ input { } header { - background-color: #0082c9; - background-image: linear-gradient(40deg, #0082c9 0%, #30b6ff 100%); + position: fixed; + top: 0; + width: 100%; + background-color: transparent; height: 50px; justify-content: space-between; + align-items: center; display: flex; + padding: 0 20px; + z-index: 1000; +} + +header > form { + margin-left: auto; + margin-right: 30px; +} + +/* Standard styling for enabled checkboxes */ +input[type="checkbox"]:not(:disabled) { + width: var(--checkbox-size); + height: var(--checkbox-size); + -webkit-appearance: none; /* remove default styling */ + -moz-appearance: none; + appearance: none; + border: 1px solid var(--color-primary-element); + border-radius: 2px; + cursor: pointer; + position: relative; + vertical-align: middle; /* align checkbox vertically with text */ + margin-top: -1px; /* adjust for better alignment */ +} + +/* Hover effects for enabled checkboxes */ +input[type="checkbox"]:not(:disabled):hover { + border-color: var(--color-primary-element-hover); +} + +/* Checkmark styling for enabled checkboxes */ +input[type="checkbox"]:checked:not(:disabled) { + background-color: var(--color-primary-element); + border-color: var(--color-border-maxcontrast); +} + +input[type="checkbox"]:checked:not(:disabled)::after { + content: ''; /* Creates a pseudo-element for the checkmark */ + position: absolute; /* Positions it absolutely */ + left: 4px; /* Positioning of the checkmark */ + top: 0; /* Positioning of the checkmark */ + width: 4px; /* Width of the checkmark */ + height: 9px; /* Height of the checkmark */ + border: solid white; /* Color of the checkmark */ + border-width: 0 2px 3px 0; /* Creates the checkmark shape */ + transform: rotate(45deg); /* Rotates to form a checkmark */ +} + +/* Styling for disabled checkboxes (grayed out, no hover, no pointer) */ +input[type="checkbox"]:disabled:not(:checked) { + background-color: var(--color-disabled); + border-color: var(--color-border-disabled); + cursor: default; + opacity: 0.5; /* Makes the checkbox appear faded */ +} + +/* Styling for disabled checked checkboxes (no pointer) */ +input[type="checkbox"]:disabled:checked { + cursor: default; +} + +input[type="checkbox"]:disabled:hover { + border-color: var(--color-border-disabled); /* Keeps disabled state without hover effect */ +} + +/* General Label styling */ +label { + cursor: pointer; + margin-left: 4px; + line-height: var(--checkbox-size); +} + +/* Label cursor for disabled checkboxes */ +input[type="checkbox"]:disabled + label { + cursor: default; +} + +/* Label styling for disabled, not checked checkboxes */ +input[type="checkbox"]:disabled:not(:checked) + label { + color: var(--color-text-disabled); } .loading { @@ -171,24 +457,24 @@ header { } #overlay { - position: fixed; /* Sit on top of the page content */ - display: none; /* Hidden by default */ - width: 100%; /* Full width (cover the whole page) */ - height: 100%; /* Full height (cover the whole page) */ - top: 0; - left: 0; - background-color: rgba(0,0,0,0.5); /* Black background with opacity */ - z-index: 2; + position: fixed; /* Sit on top of the page content */ + display: none; /* Hidden by default */ + width: 100%; /* Full width (cover the whole page) */ + height: 100%; /* Full height (cover the whole page) */ + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.5); /* Black background with opacity */ + z-index: 2; } #overlay.loading { - display: block; + display: block; } .loader { - border: 16px solid #f3f3f3; + border: 16px solid var(--color-loader); border-radius: 50%; - border-top: 16px solid #3498db; + border-top: 16px solid var(--color-nextcloud-blue); width: 120px; height: 120px; -webkit-animation: spin 2s linear infinite; /* Safari */ @@ -196,15 +482,227 @@ header { position: absolute; top: calc(50% - 60px); left: calc(50% - 60px); - } - - /* Safari */ - @-webkit-keyframes spin { +} + +/* Safari */ +@-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } - } - - @keyframes spin { +} + +@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } - } +} + +/* General theme button styling */ +#theme-toggle { + position: fixed; /* Keep the button in the same position */ + right: 30px; /* Adjust the distance from the right */ + bottom: 30px; /* Adjust the distance from the bottom */ + background-color: transparent; /* Make the background transparent */ + border: none; /* Remove border */ + font-size: 36px; /* Adjust font size */ + cursor: pointer; /* Change cursor to pointer */ + outline: none; + z-index: 9999; /* Ensures the icon is on top of every layer */ +} + +/* Icon styling: default state */ +#theme-icon { + display: inline-block; + border-radius: 50%; /* Round shape */ + position: relative; /* For the pseudo-element positioning */ + transition: box-shadow 0.3s, background-color 0.3s; /* Smooth transition for hover effect */ + opacity: 0.6; /* Slightly transparent by default */ + filter: grayscale(100%); /* Make the icon black and white */ +} + +/* Create the inner glow effect with ::after */ +#theme-icon::after { + content: ''; /* Empty content for the pseudo-element */ + position: absolute; + top: 50%; + left: 50%; + width: 0px; /* Invisible dot */ + height: 0px; /* Invisible dot */ + background-color: transparent; /* Invisible by default */ + border-radius: 50%; /* Circle shape */ + transform: translate(-50%, -50%); /* Center the dot */ + transition: box-shadow 0.3s, background-color 0.3s; /* Smooth transition for hover */ +} + +/* Hover effect for both light and dark modes */ +#theme-toggle:hover #theme-icon { + position: relative; /* Ensures stacking order */ + filter: grayscale(0%); /* Restore full color */ + opacity: 1; /* Fully visible on hover */ +} + +/* Inner glow when hovered */ +#theme-toggle:hover #theme-icon::after { + box-shadow: 0 0 40px 40px rgba(128, 128, 128, 0.4); /* Blur effect from inside */ + background-color: rgba(128, 128, 128, 0.2); /* Light glow inside */ +} + +/* Remove hover effects when not hovering */ +#theme-toggle:not(:hover) #theme-icon { + opacity: 0.6; /* Slightly transparent */ +} +/* Office Suite Feature Cards */ +.office-suite-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin: 20px 0; + align-items: stretch; +} + +.office-radio { + display: none; +} + +.office-card { + position: relative; + border: 2px solid var(--color-border-maxcontrast); + border-radius: var(--border-radius-large); + padding: 20px; + cursor: pointer; + transition: all 0.3s ease; + background-color: var(--color-main-background); + display: flex; + flex-direction: column; +} + +.office-card-disabled { + opacity: 50%; + pointer-events: none; +} + +.office-card:hover { + border-color: var(--color-primary-element); + box-shadow: 0 4px 12px rgba(0, 130, 201, 0.15); + transform: translateY(-2px); +} + +#office-collabora:checked + .office-card, +#office-onlyoffice:checked + .office-card { + border-color: var(--color-nextcloud-blue); + background: linear-gradient(135deg, rgba(0, 130, 201, 0.08) 0%, rgba(0, 130, 201, 0.02) 100%); +} + +[data-theme="dark"] #office-collabora:checked + .office-card, +[data-theme="dark"] #office-onlyoffice:checked + .office-card { + background: linear-gradient(135deg, rgba(0, 145, 242, 0.15) 0%, rgba(0, 145, 242, 0.03) 100%); +} + +.office-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.office-card h4 { + margin: 0; + height: 24px; + font-size: 18px; + font-weight: 600; + color: var(--color-main-text); +} + +.office-checkmark { + flex-shrink: 0; + display: none; +} + +#office-collabora:checked + .office-card .office-checkmark, +#office-onlyoffice:checked + .office-card .office-checkmark { + display: block; +} + +.office-features { + list-style: none; + padding: 0; + margin: 0; +} + +.office-features li { + position: relative; + padding-left: 20px; + margin-bottom: 4px; + font-size: var(--default-font-size); + line-height: 1.5; + color: var(--color-main-text); +} + +.office-features li::before { + content: '•'; + position: absolute; + left: 6px; + color: var(--color-nextcloud-blue); + font-weight: bold; +} + +.office-checkbox { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.office-learn-more { + display: inline-flex; + align-items: center; + margin-top: 12px; + color: var(--color-primary-element); + text-decoration: none; + font-size: var(--default-font-size); + font-weight: 500; + transition: color 0.2s ease; +} + +.office-learn-more:hover { + color: var(--color-primary-element-hover); +} + +.office-learn-more svg { + transition: transform 0.2s ease; +} + +.office-learn-more:hover svg { + transform: translateX(3px); +} + +.office-none-card { + text-align: center; + margin: 12px 0 20px 0; +} + +.office-none-label { + display: inline-flex; + align-items: center; + font-size: 13px; + color: var(--color-primary-element); + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s ease; + padding: 8px 12px; + border-radius: var(--border-radius); +} + +.office-none-label:hover { + opacity: 1; + background-color: var(--color-primary-element-light); +} + +#office-none:checked + .office-none-label { + opacity: 1; + font-weight: 600; +} + +/* Responsive adjustments for mobile */ +@media only screen and (max-width: 800px) { + .office-suite-cards { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/php/public/timezone.js b/php/public/timezone.js new file mode 100644 index 00000000..b43bdfe7 --- /dev/null +++ b/php/public/timezone.js @@ -0,0 +1,7 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // timezone + let timezone = document.getElementById("timezone"); + if (timezone) { + timezone.value = Intl.DateTimeFormat().resolvedOptions().timeZone + } +}); diff --git a/php/public/toggle-dark-mode.js b/php/public/toggle-dark-mode.js new file mode 100644 index 00000000..9df54287 --- /dev/null +++ b/php/public/toggle-dark-mode.js @@ -0,0 +1,37 @@ +// Function to toggle theme +function toggleTheme() { + const currentTheme = document.documentElement.getAttribute('data-theme'); + const newTheme = (currentTheme === 'dark') ? '' : 'dark'; // Toggle between no theme and dark theme + document.documentElement.setAttribute('data-theme', newTheme); + localStorage.setItem('theme', newTheme); + + // Change the icon based on the current theme + const themeIcon = document.getElementById('theme-icon'); + themeIcon.textContent = newTheme === 'dark' ? '☀️' : '🌙'; // Switch between moon and sun icons +} + +// Function to immediately apply saved theme without icon update +function applySavedThemeImmediately() { + const savedTheme = localStorage.getItem('theme'); + if (savedTheme === 'dark') { + document.documentElement.setAttribute('data-theme', 'dark'); + } else { + document.documentElement.removeAttribute('data-theme'); // Default to light theme + } +} + +// Function to apply theme-icon update +function setThemeIcon() { + const savedTheme = localStorage.getItem('theme'); + if (savedTheme === 'dark') { + document.getElementById('theme-icon').textContent = '☀️'; // Sun icon for dark mode + } else { + document.getElementById('theme-icon').textContent = '🌙'; // Moon icon for light mode + } +} + +// Immediately apply the saved theme to avoid flickering +applySavedThemeImmediately(); + +// Apply theme when the page loads +document.addEventListener('DOMContentLoaded', setThemeIcon); diff --git a/php/src/Auth/AuthManager.php b/php/src/Auth/AuthManager.php index f18f1a7b..f6ab0d10 100644 --- a/php/src/Auth/AuthManager.php +++ b/php/src/Auth/AuthManager.php @@ -3,24 +3,40 @@ namespace AIO\Auth; use AIO\Data\ConfigurationManager; +use AIO\Data\DataConst; +use \DateTime; -class AuthManager { - private const SESSION_KEY = 'aio_authenticated'; - private ConfigurationManager $configurationManager; +readonly class AuthManager { + private const string SESSION_KEY = 'aio_authenticated'; - public function __construct(ConfigurationManager $configurationManager) { - $this->configurationManager = $configurationManager; + public function __construct( + private ConfigurationManager $configurationManager + ) { } public function CheckCredentials(string $password) : bool { - return hash_equals($this->configurationManager->GetPassword(), $password); + return hash_equals($this->configurationManager->password, $password); } public function CheckToken(string $token) : bool { - return hash_equals($this->configurationManager->GetToken(), $token); + return hash_equals($this->configurationManager->aioToken, $token); } public function SetAuthState(bool $isLoggedIn) : void { + + if (!$this->IsAuthenticated() && $isLoggedIn === true) { + $date = new DateTime(); + $dateTime = $date->getTimestamp(); + $_SESSION['date_time'] = $dateTime; + + $df = disk_free_space(DataConst::GetSessionDirectory()); + if ($df !== false && (int)$df < 10240) { + error_log(DataConst::GetSessionDirectory() . " has only less than 10KB free space. The login might not succeed because of that!"); + } + + file_put_contents(DataConst::GetSessionDateFile(), (string)$dateTime); + } + $_SESSION[self::SESSION_KEY] = $isLoggedIn; } diff --git a/php/src/Container/AioVariables.php b/php/src/Container/AioVariables.php new file mode 100644 index 00000000..10a150f4 --- /dev/null +++ b/php/src/Container/AioVariables.php @@ -0,0 +1,19 @@ +variables[] = $variable; + } + + /** + * @return string[] + */ + public function GetVariables() : array { + return $this->variables; + } +} diff --git a/php/src/Container/Container.php b/php/src/Container/Container.php index c1330da3..6e5d2b54 100644 --- a/php/src/Container/Container.php +++ b/php/src/Container/Container.php @@ -2,115 +2,68 @@ namespace AIO\Container; -use AIO\Container\State\IContainerState; use AIO\Data\ConfigurationManager; use AIO\Docker\DockerActionManager; use AIO\ContainerDefinitionFetcher; +use JsonException; -class Container { - private string $identifier; - private string $displayName; - private string $containerName; - private string $restartPolicy; - private int $maxShutdownTime; - private ContainerPorts $ports; - private ContainerInternalPorts $internalPorts; - private ContainerVolumes $volumes; - private ContainerEnvironmentVariables $containerEnvironmentVariables; - /** @var string[] */ - private array $dependsOn; - /** @var string[] */ - private array $secrets; - private DockerActionManager $dockerActionManager; - +readonly class Container { public function __construct( - string $identifier, - string $displayName, - string $containerName, - string $restartPolicy, - int $maxShutdownTime, - ContainerPorts $ports, - ContainerInternalPorts $internalPorts, - ContainerVolumes $volumes, - ContainerEnvironmentVariables $containerEnvironmentVariables, - array $dependsOn, - array $secrets, - DockerActionManager $dockerActionManager + public string $identifier, + public string $displayName, + public string $containerName, + public string $restartPolicy, + public int $maxShutdownTime, + public ContainerPorts $ports, + public string $internalPorts, + public ContainerVolumes $volumes, + public ContainerEnvironmentVariables $containerEnvironmentVariables, + /** @var string[] */ + public array $dependsOn, + private string $uiSecret, + /** @var string[] */ + public array $devices, + public bool $enableNvidiaGpu, + /** @var string[] */ + public array $capAdd, + public int $shmSize, + public bool $apparmorUnconfined, + /** @var string[] */ + public array $backupVolumes, + public array $nextcloudExecCommands, + public bool $readOnlyRootFs, + public array $tmpfs, + public bool $init, + public string $imageTag, + public AioVariables $aioVariables, + public string $documentation, + private DockerActionManager $dockerActionManager ) { - $this->identifier = $identifier; - $this->displayName = $displayName; - $this->containerName = $containerName; - $this->restartPolicy = $restartPolicy; - $this->maxShutdownTime = $maxShutdownTime; - $this->ports = $ports; - $this->internalPorts = $internalPorts; - $this->volumes = $volumes; - $this->containerEnvironmentVariables = $containerEnvironmentVariables; - $this->dependsOn = $dependsOn; - $this->secrets = $secrets; - $this->dockerActionManager = $dockerActionManager; } - public function GetIdentifier() : string { - return $this->identifier; - } - - public function GetDisplayName() : string { - return $this->displayName; - } - - public function GetContainerName() : string { - return $this->containerName; - } - - public function GetRestartPolicy() : string { - return $this->restartPolicy; - } - - public function GetMaxShutdownTime() : int { - return $this->maxShutdownTime; - } - - public function GetSecrets() : array { - return $this->secrets; - } - - public function GetPorts() : ContainerPorts { - return $this->ports; - } - - public function GetInternalPorts() : ContainerInternalPorts { - return $this->internalPorts; - } - - public function GetVolumes() : ContainerVolumes { - return $this->volumes; - } - - public function GetRunningState() : IContainerState { - return $this->dockerActionManager->GetContainerRunningState($this); - } - - public function GetRestartingState() : IContainerState { - return $this->dockerActionManager->GetContainerRestartingState($this); - } - - public function GetUpdateState() : IContainerState { - return $this->dockerActionManager->GetContainerUpdateState($this); - } - - public function GetStartingState() : IContainerState { - return $this->dockerActionManager->GetContainerStartingState($this); + public function GetUiSecret() : string { + return $this->dockerActionManager->GetAndGenerateSecretWrapper($this->uiSecret); } /** - * @return string[] + * @throws JsonException */ - public function GetDependsOn() : array { - return $this->dependsOn; + public function GetRunningState() : ContainerState { + return $this->dockerActionManager->GetContainerRunningState($this); } - public function GetEnvironmentVariables() : ContainerEnvironmentVariables { - return $this->containerEnvironmentVariables; + /** + * @throws JsonException + */ + public function GetRestartingState() : ContainerState { + return $this->dockerActionManager->GetContainerRestartingState($this); + } + + public function GetUpdateState() : VersionState { + return $this->dockerActionManager->GetContainerUpdateState($this); + } + + public function GetStartingState() : ContainerState { + return $this->dockerActionManager->GetContainerStartingState($this); } } diff --git a/php/src/Container/ContainerInternalPorts.php b/php/src/Container/ContainerInternalPorts.php deleted file mode 100644 index fb0716bf..00000000 --- a/php/src/Container/ContainerInternalPorts.php +++ /dev/null @@ -1,19 +0,0 @@ -internalPorts[] = $internalPort; - } - - /** - * @return string[] - */ - public function GetInternalPorts() : array { - return $this->internalPorts; - } -} diff --git a/php/src/Container/ContainerPort.php b/php/src/Container/ContainerPort.php new file mode 100644 index 00000000..ff8a6250 --- /dev/null +++ b/php/src/Container/ContainerPort.php @@ -0,0 +1,12 @@ +ports[] = $port; } /** - * @return string[] + * @return ContainerPort[] */ public function GetPorts() : array { return $this->ports; } -} +} \ No newline at end of file diff --git a/php/src/Container/ContainerState.php b/php/src/Container/ContainerState.php new file mode 100644 index 00000000..f6481027 --- /dev/null +++ b/php/src/Container/ContainerState.php @@ -0,0 +1,12 @@ +name = $name; - $this->mountPoint = $mountPoint; - $this->isWritable = $isWritable; } } diff --git a/php/src/Container/State/IContainerState.php b/php/src/Container/State/IContainerState.php deleted file mode 100644 index d93dab65..00000000 --- a/php/src/Container/State/IContainerState.php +++ /dev/null @@ -1,5 +0,0 @@ -configurationManager = $configurationManager; - $this->container = $container; + private ConfigurationManager $configurationManager, + private \DI\Container $container + ) { } public function GetContainerById(string $id): Container @@ -32,7 +25,7 @@ class ContainerDefinitionFetcher $containers = $this->FetchDefinition(); foreach ($containers as $container) { - if ($container->GetIdentifier() === $id) { + if ($container->identifier === $id) { return $container; } } @@ -43,146 +36,310 @@ class ContainerDefinitionFetcher /** * @return array */ - private function GetDefinition(bool $latest): array + private function GetDefinition(): array { - $data = json_decode(file_get_contents(__DIR__ . '/../containers.json'), true); + $data = json_decode((string)file_get_contents(DataConst::GetContainersDefinitionPath()), true, 512, JSON_THROW_ON_ERROR); + + $additionalContainerNames = []; + foreach ($this->configurationManager->aioCommunityContainers as $communityContainer) { + if ($communityContainer !== '') { + $path = DataConst::GetCommunityContainersDirectory() . '/' . $communityContainer . '/' . $communityContainer . '.json'; + $additionalData = json_decode((string)file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); + $data = array_merge_recursive($data, $additionalData); + if (isset($additionalData['aio_services_v1'][0]['display_name']) && $additionalData['aio_services_v1'][0]['display_name'] !== '') { + // Store container_name of community containers in variable for later + $additionalContainerNames[] = $additionalData['aio_services_v1'][0]['container_name']; + } + } + } $containers = []; - foreach ($data['production'] as $entry) { - if ($entry['identifier'] === 'nextcloud-aio-clamav') { - if (!$this->configurationManager->isClamavEnabled()) { + foreach ($data['aio_services_v1'] as $entry) { + if ($entry['container_name'] === 'nextcloud-aio-clamav') { + if (!$this->configurationManager->isClamavEnabled) { continue; } - } elseif ($entry['identifier'] === 'nextcloud-aio-onlyoffice') { - if (!$this->configurationManager->isOnlyofficeEnabled()) { + } elseif ($entry['container_name'] === 'nextcloud-aio-onlyoffice') { + if (!$this->configurationManager->isOnlyofficeEnabled) { continue; } - } elseif ($entry['identifier'] === 'nextcloud-aio-collabora') { - if (!$this->configurationManager->isCollaboraEnabled()) { + } elseif ($entry['container_name'] === 'nextcloud-aio-collabora') { + if (!$this->configurationManager->isCollaboraEnabled) { continue; } - } elseif ($entry['identifier'] === 'nextcloud-aio-talk') { - if (!$this->configurationManager->isTalkEnabled()) { + if ($this->configurationManager->isCollaboraSubscriptionEnabled()) { + $entry['image'] = 'ghcr.io/nextcloud-releases/aio-collabora-online'; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-talk') { + if (!$this->configurationManager->isTalkEnabled) { continue; } - } elseif ($entry['identifier'] === 'nextcloud-aio-imaginary') { - if (!$this->configurationManager->isImaginaryEnabled()) { + } elseif ($entry['container_name'] === 'nextcloud-aio-talk-recording') { + if (!$this->configurationManager->isTalkRecordingEnabled) { continue; } - } elseif ($entry['identifier'] === 'nextcloud-aio-fulltextsearch') { - if (!$this->configurationManager->isFulltextsearchEnabled()) { + } elseif ($entry['container_name'] === 'nextcloud-aio-imaginary') { + if (!$this->configurationManager->isImaginaryEnabled) { + continue; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-fulltextsearch') { + if (!$this->configurationManager->isFulltextsearchEnabled) { + continue; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-docker-socket-proxy') { + if (!$this->configurationManager->isDockerSocketProxyEnabled) { + continue; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-whiteboard') { + if (!$this->configurationManager->isWhiteboardEnabled) { continue; } } $ports = new ContainerPorts(); - foreach ($entry['ports'] as $port) { - if($port === '%APACHE_PORT%/tcp') { - $port = $this->configurationManager->GetApachePort() . '/tcp'; - } elseif($port === '%TALK_PORT%/tcp') { - $port = $this->configurationManager->GetTalkPort() . '/tcp'; - } elseif($port === '%TALK_PORT%/udp') { - $port = $this->configurationManager->GetTalkPort() . '/udp'; + if (isset($entry['ports'])) { + foreach ($entry['ports'] as $value) { + $ports->AddPort( + new ContainerPort( + $value['port_number'], + $value['ip_binding'], + $value['protocol'] + ) + ); } - $ports->AddPort($port); - } - - $internalPorts = new ContainerInternalPorts(); - foreach ($entry['internalPorts'] as $internalPort) { - if($internalPort === '%APACHE_PORT%') { - $internalPort = $this->configurationManager->GetApachePort(); - } elseif($internalPort === '%TALK_PORT%') { - $internalPort = $this->configurationManager->GetTalkPort(); - } - $internalPorts->AddInternalPort($internalPort); } $volumes = new ContainerVolumes(); - foreach ($entry['volumes'] as $value) { - if($value['name'] === '%BORGBACKUP_HOST_LOCATION%') { - $value['name'] = $this->configurationManager->GetBorgBackupHostLocation(); - if($value['name'] === '') { - continue; + if (isset($entry['volumes'])) { + foreach ($entry['volumes'] as $value) { + if($value['source'] === '%BORGBACKUP_HOST_LOCATION%') { + $value['source'] = $this->configurationManager->borgBackupHostLocation; + if($value['source'] === '') { + continue; + } } + if($value['source'] === '%NEXTCLOUD_MOUNT%') { + $value['source'] = $this->configurationManager->nextcloudMount; + if($value['source'] === '') { + continue; + } + } elseif ($value['source'] === '%NEXTCLOUD_DATADIR%') { + $value['source'] = $this->configurationManager->nextcloudDatadirMount; + if ($value['source'] === '') { + continue; + } + } elseif ($value['source'] === '%WATCHTOWER_DOCKER_SOCKET_PATH%') { + $value['source'] = $this->configurationManager->dockerSocketPath; + if($value['source'] === '') { + continue; + } + } elseif ($value['source'] === '%NEXTCLOUD_TRUSTED_CACERTS_DIR%') { + $value['source'] = $this->configurationManager->trustedCacertsDir; + if($value['source'] === '') { + continue; + } + } + if ($value['destination'] === '%NEXTCLOUD_MOUNT%') { + $value['destination'] = $this->configurationManager->nextcloudMount; + if($value['destination'] === '') { + continue; + } + } + $volumes->AddVolume( + new ContainerVolume( + $value['source'], + $value['destination'], + $value['writeable'] + ) + ); } - if($value['name'] === '%NEXTCLOUD_MOUNT%') { - $value['name'] = $this->configurationManager->GetNextcloudMount(); - if($value['name'] === '') { - continue; - } - } elseif ($value['name'] === '%NEXTCLOUD_DATADIR%') { - $value['name'] = $this->configurationManager->GetNextcloudDatadirMount(); - if ($value['name'] === '') { - continue; - } - } elseif ($value['name'] === '%DOCKER_SOCKET_PATH%') { - $value['name'] = $this->configurationManager->GetDockerSocketPath(); - if($value['name'] === '') { - continue; - } - } - if ($value['location'] === '%NEXTCLOUD_MOUNT%') { - $value['location'] = $this->configurationManager->GetNextcloudMount(); - if($value['location'] === '') { - continue; - } - } - $volumes->AddVolume( - new ContainerVolume( - $value['name'], - $value['location'], - $value['writeable'] - ) - ); } $dependsOn = []; - foreach ($entry['dependsOn'] as $value) { - if ($value === 'nextcloud-aio-clamav') { - if (!$this->configurationManager->isClamavEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-onlyoffice') { - if (!$this->configurationManager->isOnlyofficeEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-collabora') { - if (!$this->configurationManager->isCollaboraEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-talk') { - if (!$this->configurationManager->isTalkEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-imaginary') { - if (!$this->configurationManager->isImaginaryEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-fulltextsearch') { - if (!$this->configurationManager->isFulltextsearchEnabled()) { - continue; + if (isset($entry['depends_on'])) { + $valueDependsOn = $entry['depends_on']; + if ($entry['container_name'] === 'nextcloud-aio-apache') { + // Add community containers first and default ones last so that aio_variables works correctly + $valueDependsOnTemp = []; + foreach ($additionalContainerNames as $containerName) { + $valueDependsOnTemp[] = $containerName; } + $valueDependsOn = array_merge_recursive($valueDependsOnTemp, $valueDependsOn); + } + foreach ($valueDependsOn as $value) { + if ($value === 'nextcloud-aio-clamav') { + if (!$this->configurationManager->isClamavEnabled) { + continue; + } + } elseif ($value === 'nextcloud-aio-onlyoffice') { + if (!$this->configurationManager->isOnlyofficeEnabled) { + continue; + } + } elseif ($value === 'nextcloud-aio-collabora') { + if (!$this->configurationManager->isCollaboraEnabled) { + continue; + } + } elseif ($value === 'nextcloud-aio-talk') { + if (!$this->configurationManager->isTalkEnabled) { + continue; + } + } elseif ($value === 'nextcloud-aio-talk-recording') { + if (!$this->configurationManager->isTalkRecordingEnabled) { + continue; + } + } elseif ($value === 'nextcloud-aio-imaginary') { + if (!$this->configurationManager->isImaginaryEnabled) { + continue; + } + } elseif ($value === 'nextcloud-aio-fulltextsearch') { + if (!$this->configurationManager->isFulltextsearchEnabled) { + continue; + } + } elseif ($value === 'nextcloud-aio-docker-socket-proxy') { + if (!$this->configurationManager->isDockerSocketProxyEnabled) { + continue; + } + } elseif ($value === 'nextcloud-aio-whiteboard') { + if (!$this->configurationManager->isWhiteboardEnabled) { + continue; + } + } + $dependsOn[] = $value; } - $dependsOn[] = $value; } - + $variables = new ContainerEnvironmentVariables(); - foreach ($entry['environmentVariables'] as $value) { - $variables->AddVariable($value); + if (isset($entry['environment'])) { + foreach ($entry['environment'] as $value) { + $variables->AddVariable($value); + } + } + + $aioVariables = new AioVariables(); + if (isset($entry['aio_variables'])) { + foreach ($entry['aio_variables'] as $value) { + $aioVariables->AddVariable($value); + } + } + + $displayName = ''; + if (isset($entry['display_name'])) { + $displayName = $entry['display_name']; + } + + $restartPolicy = ''; + if (isset($entry['restart'])) { + $restartPolicy = $entry['restart']; + } + + $maxShutdownTime = 10; + if (isset($entry['stop_grace_period'])) { + $maxShutdownTime = $entry['stop_grace_period']; + } + + $internalPort = ''; + if (isset($entry['internal_port'])) { + $internalPort = $entry['internal_port']; + } + + if (isset($entry['secrets'])) { + // All secrets are registered with the configuration when they + // are discovered so they can be later generated at time-of-use. + foreach ($entry['secrets'] as $secret) { + $this->configurationManager->registerSecret($secret); + } + } + + $uiSecret = ''; + if (isset($entry['ui_secret'])) { + $uiSecret = $entry['ui_secret']; + } + + $devices = []; + if (isset($entry['devices'])) { + $devices = $entry['devices']; + } + + $enableNvidiaGpu = false; + if (isset($entry['enable_nvidia_gpu'])) { + $enableNvidiaGpu = $entry['enable_nvidia_gpu']; + } + + $capAdd = []; + if (isset($entry['cap_add'])) { + $capAdd = $entry['cap_add']; + } + + $shmSize = -1; + if (isset($entry['shm_size'])) { + $shmSize = $entry['shm_size']; + } + + $apparmorUnconfined = false; + if (isset($entry['apparmor_unconfined'])) { + $apparmorUnconfined = $entry['apparmor_unconfined']; + } + + $backupVolumes = []; + if (isset($entry['backup_volumes'])) { + $backupVolumes = $entry['backup_volumes']; + } + + $nextcloudExecCommands = []; + if (isset($entry['nextcloud_exec_commands'])) { + $nextcloudExecCommands = $entry['nextcloud_exec_commands']; + } + + $readOnlyRootFs = false; + if (isset($entry['read_only'])) { + $readOnlyRootFs = $entry['read_only']; + } + + $tmpfs = []; + if (isset($entry['tmpfs'])) { + $tmpfs = $entry['tmpfs']; + } + + $init = true; + if (isset($entry['init'])) { + $init = $entry['init']; + } + + $imageTag = '%AIO_CHANNEL%'; + if (isset($entry['image_tag'])) { + $imageTag = $entry['image_tag']; + } + + $documentation = ''; + if (isset($entry['documentation'])) { + $documentation = $entry['documentation']; } $containers[] = new Container( - $entry['identifier'], - $entry['displayName'], - $entry['containerName'], - $entry['restartPolicy'], - $entry['maxShutdownTime'], + $entry['container_name'], + $displayName, + $entry['image'], + $restartPolicy, + $maxShutdownTime, $ports, - $internalPorts, + $internalPort, $volumes, $variables, $dependsOn, - $entry['secrets'], + $uiSecret, + $devices, + $enableNvidiaGpu, + $capAdd, + $shmSize, + $apparmorUnconfined, + $backupVolumes, + $nextcloudExecCommands, + $readOnlyRootFs, + $tmpfs, + $init, + $imageTag, + $aioVariables, + $documentation, $this->container->get(DockerActionManager::class) ); } @@ -192,35 +349,6 @@ class ContainerDefinitionFetcher public function FetchDefinition(): array { - if (!file_exists(DataConst::GetDataDirectory() . '/containers.json')) { - $containers = $this->GetDefinition(true); - } else { - $containers = $this->GetDefinition(false); - } - - $borgBackupMode = $this->configurationManager->GetBorgBackupMode(); - $fetchLatest = false; - - foreach ($containers as $container) { - - if ($container->GetIdentifier() === 'nextcloud-aio-borgbackup') { - if ($container->GetRunningState() === RunningState::class) { - if ($borgBackupMode !== 'backup' && $borgBackupMode !== 'restore') { - $fetchLatest = true; - } - } else { - $fetchLatest = true; - } - - } elseif ($container->GetIdentifier() === 'nextcloud-aio-watchtower' && $container->GetRunningState() === RunningState::class) { - return $containers; - } - } - - if ($fetchLatest === true) { - $containers = $this->GetDefinition(true); - } - - return $containers; + return $this->GetDefinition(); } } diff --git a/php/src/Controller/ConfigurationController.php b/php/src/Controller/ConfigurationController.php index a06cc6b9..c40ee98c 100644 --- a/php/src/Controller/ConfigurationController.php +++ b/php/src/Controller/ConfigurationController.php @@ -9,38 +9,38 @@ use AIO\Docker\DockerActionManager; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; -class ConfigurationController -{ - private ConfigurationManager $configurationManager; - +readonly class ConfigurationController { public function __construct( - ConfigurationManager $configurationManager + private ConfigurationManager $configurationManager ) { - $this->configurationManager = $configurationManager; } - public function SetConfig(Request $request, Response $response, $args) : Response { + public function SetConfig(Request $request, Response $response, array $args): Response { try { + $this->configurationManager->startTransaction(); if (isset($request->getParsedBody()['domain'])) { $domain = $request->getParsedBody()['domain'] ?? ''; - $this->configurationManager->SetDomain($domain); + $skipDomainValidation = isset($request->getParsedBody()['skip_domain_validation']); + $this->configurationManager->setDomain($domain, $skipDomainValidation); } if (isset($request->getParsedBody()['current-master-password']) || isset($request->getParsedBody()['new-master-password'])) { $currentMasterPassword = $request->getParsedBody()['current-master-password'] ?? ''; $newMasterPassword = $request->getParsedBody()['new-master-password'] ?? ''; - $this->configurationManager->ChangeMasterPassword($currentMasterPassword, $newMasterPassword); + $this->configurationManager->changeMasterPassword($currentMasterPassword, $newMasterPassword); } - if (isset($request->getParsedBody()['borg_backup_host_location'])) { + if (isset($request->getParsedBody()['borg_backup_host_location']) || isset($request->getParsedBody()['borg_remote_repo'])) { $location = $request->getParsedBody()['borg_backup_host_location'] ?? ''; - $this->configurationManager->SetBorgBackupHostLocation($location); + $borgRemoteRepo = $request->getParsedBody()['borg_remote_repo'] ?? ''; + $this->configurationManager->setBorgLocationVars($location, $borgRemoteRepo); } - if (isset($request->getParsedBody()['borg_restore_host_location']) || isset($request->getParsedBody()['borg_restore_password'])) { + if (isset($request->getParsedBody()['borg_restore_host_location']) || isset($request->getParsedBody()['borg_restore_remote_repo']) || isset($request->getParsedBody()['borg_restore_password'])) { $restoreLocation = $request->getParsedBody()['borg_restore_host_location'] ?? ''; + $borgRemoteRepo = $request->getParsedBody()['borg_restore_remote_repo'] ?? ''; $borgPassword = $request->getParsedBody()['borg_restore_password'] ?? ''; - $this->configurationManager->SetBorgRestoreHostLocationAndPassword($restoreLocation, $borgPassword); + $this->configurationManager->setBorgRestoreLocationVarsAndPassword($restoreLocation, $borgRemoteRepo, $borgPassword); } if (isset($request->getParsedBody()['daily_backup_time'])) { @@ -49,77 +49,97 @@ class ConfigurationController } else { $enableAutomaticUpdates = false; } + if (isset($request->getParsedBody()['success_notification'])) { + $successNotification = true; + } else { + $successNotification = false; + } $dailyBackupTime = $request->getParsedBody()['daily_backup_time'] ?? ''; - $this->configurationManager->SetDailyBackupTime($dailyBackupTime, $enableAutomaticUpdates); + $this->configurationManager->setDailyBackupTime($dailyBackupTime, $enableAutomaticUpdates, $successNotification); } if (isset($request->getParsedBody()['delete_daily_backup_time'])) { - $this->configurationManager->DeleteDailyBackupTime(); + $this->configurationManager->deleteDailyBackupTime(); } if (isset($request->getParsedBody()['additional_backup_directories'])) { $additionalBackupDirectories = $request->getParsedBody()['additional_backup_directories'] ?? ''; - $this->configurationManager->SetAdditionalBackupDirectories($additionalBackupDirectories); + $this->configurationManager->setAdditionalBackupDirectories($additionalBackupDirectories); } if (isset($request->getParsedBody()['delete_timezone'])) { - $this->configurationManager->DeleteTimezone(); + $this->configurationManager->deleteTimezone(); } if (isset($request->getParsedBody()['timezone'])) { $timezone = $request->getParsedBody()['timezone'] ?? ''; - $this->configurationManager->SetTimezone($timezone); + $this->configurationManager->timezone = $timezone; } if (isset($request->getParsedBody()['options-form'])) { - if (isset($request->getParsedBody()['collabora']) && isset($request->getParsedBody()['onlyoffice'])) { - throw new InvalidSettingConfigurationException("Collabora and Onlyoffice are not allowed to be enabled at the same time!"); - } - if (isset($request->getParsedBody()['clamav'])) { - $this->configurationManager->SetClamavEnabledState(1); + $officeSuiteChoice = $request->getParsedBody()['office_suite_choice'] ?? ''; + + if ($officeSuiteChoice === 'collabora') { + $this->configurationManager->isCollaboraEnabled = true; + $this->configurationManager->isOnlyofficeEnabled = false; + } elseif ($officeSuiteChoice === 'onlyoffice') { + $this->configurationManager->isCollaboraEnabled = false; + $this->configurationManager->isOnlyofficeEnabled = true; } else { - $this->configurationManager->SetClamavEnabledState(0); + $this->configurationManager->isCollaboraEnabled = false; + $this->configurationManager->isOnlyofficeEnabled = false; } - if (isset($request->getParsedBody()['onlyoffice'])) { - $this->configurationManager->SetOnlyofficeEnabledState(1); - } else { - $this->configurationManager->SetOnlyofficeEnabledState(0); - } - if (isset($request->getParsedBody()['collabora'])) { - $this->configurationManager->SetCollaboraEnabledState(1); - } else { - $this->configurationManager->SetCollaboraEnabledState(0); - } - if (isset($request->getParsedBody()['talk'])) { - $this->configurationManager->SetTalkEnabledState(1); - } else { - $this->configurationManager->SetTalkEnabledState(0); - } - if (isset($request->getParsedBody()['imaginary'])) { - $this->configurationManager->SetImaginaryEnabledState(1); - } else { - $this->configurationManager->SetImaginaryEnabledState(0); - } - if (isset($request->getParsedBody()['fulltextsearch'])) { - $this->configurationManager->SetFulltextsearchEnabledState(1); - } else { - $this->configurationManager->SetFulltextsearchEnabledState(0); + $this->configurationManager->isClamavEnabled = isset($request->getParsedBody()['clamav']); + $this->configurationManager->isTalkEnabled = isset($request->getParsedBody()['talk']); + $this->configurationManager->isTalkRecordingEnabled = isset($request->getParsedBody()['talk-recording']); + $this->configurationManager->isImaginaryEnabled = isset($request->getParsedBody()['imaginary']); + $this->configurationManager->isFulltextsearchEnabled = isset($request->getParsedBody()['fulltextsearch']); + $this->configurationManager->isDockerSocketProxyEnabled = isset($request->getParsedBody()['docker-socket-proxy']); + $this->configurationManager->isWhiteboardEnabled = isset($request->getParsedBody()['whiteboard']); + } + + if (isset($request->getParsedBody()['community-form'])) { + $cc = $this->configurationManager->listAvailableCommunityContainers(); + $enabledCC = []; + /** + * @psalm-suppress PossiblyNullIterator + */ + foreach ($request->getParsedBody() as $item) { + if (array_key_exists($item , $cc)) { + $enabledCC[] = $item; + } } + $this->configurationManager->aioCommunityContainers = $enabledCC; } if (isset($request->getParsedBody()['delete_collabora_dictionaries'])) { - $this->configurationManager->DeleteCollaboraDictionaries(); + $this->configurationManager->deleteCollaboraDictionaries(); } if (isset($request->getParsedBody()['collabora_dictionaries'])) { $collaboraDictionaries = $request->getParsedBody()['collabora_dictionaries'] ?? ''; - $this->configurationManager->SetCollaboraDictionaries($collaboraDictionaries); + $this->configurationManager->collaboraDictionaries = $collaboraDictionaries; } - return $response->withStatus(201)->withHeader('Location', '/'); + if (isset($request->getParsedBody()['delete_collabora_additional_options'])) { + $this->configurationManager->deleteAdditionalCollaboraOptions(); + } + + if (isset($request->getParsedBody()['collabora_additional_options'])) { + $additionalCollaboraOptions = $request->getParsedBody()['collabora_additional_options'] ?? ''; + $this->configurationManager->collaboraAdditionalOptions = $additionalCollaboraOptions; + } + + if (isset($request->getParsedBody()['delete_borg_backup_location_vars'])) { + $this->configurationManager->deleteBorgBackupLocationItems(); + } + + return $response->withStatus(201)->withHeader('Location', '.'); } catch (InvalidSettingConfigurationException $ex) { $response->getBody()->write($ex->getMessage()); return $response->withStatus(422); + } finally { + $this->configurationManager->commitTransaction(); } } } diff --git a/php/src/Controller/DockerController.php b/php/src/Controller/DockerController.php index 9db7d627..81b920d0 100644 --- a/php/src/Controller/DockerController.php +++ b/php/src/Controller/DockerController.php @@ -2,57 +2,71 @@ namespace AIO\Controller; -use AIO\Container\State\RunningState; +use AIO\Container\ContainerState; use AIO\ContainerDefinitionFetcher; use AIO\Docker\DockerActionManager; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use AIO\Data\ConfigurationManager; -class DockerController -{ - private DockerActionManager $dockerActionManager; - private ContainerDefinitionFetcher $containerDefinitionFetcher; - private const TOP_CONTAINER = 'nextcloud-aio-apache'; - private ConfigurationManager $configurationManager; +readonly class DockerController { + private const string TOP_CONTAINER = 'nextcloud-aio-apache'; public function __construct( - DockerActionManager $dockerActionManager, - ContainerDefinitionFetcher $containerDefinitionFetcher, - ConfigurationManager $configurationManager + private DockerActionManager $dockerActionManager, + private ContainerDefinitionFetcher $containerDefinitionFetcher, + private ConfigurationManager $configurationManager ) { - $this->dockerActionManager = $dockerActionManager; - $this->containerDefinitionFetcher = $containerDefinitionFetcher; - $this->configurationManager = $configurationManager; } - private function PerformRecursiveContainerStart(string $id, bool $pullContainer = true) : void { + private function PerformRecursiveContainerStart(string $id, bool $pullImage = true) : void { $container = $this->containerDefinitionFetcher->GetContainerById($id); - foreach($container->GetDependsOn() as $dependency) { - $this->PerformRecursiveContainerStart($dependency); + // Start all dependencies first and then itself + foreach($container->dependsOn as $dependency) { + $this->PerformRecursiveContainerStart($dependency, $pullImage); } - if ($id === 'nextcloud-aio-database') { - if ($this->dockerActionManager->GetDatabasecontainerExitCode() > 0) { - $pullContainer = false; - } + // Don't start if container is already running + // This is expected to happen if a container is defined in depends_on of multiple containers + if ($container->GetRunningState() === ContainerState::Running) { + error_log('Not starting ' . $id . ' because it was already started.'); + return; } + $this->dockerActionManager->DeleteContainer($container); $this->dockerActionManager->CreateVolumes($container); - if ($pullContainer) { - $this->dockerActionManager->PullContainer($container); - } else { - error_log('Not pulling the latest database image because the container was not correctly shut down.'); - } + $this->dockerActionManager->PullImage($container, $pullImage); $this->dockerActionManager->CreateContainer($container); $this->dockerActionManager->StartContainer($container); $this->dockerActionManager->ConnectContainerToNetwork($container); } - public function GetLogs(Request $request, Response $response, $args) : Response + private function PerformRecursiveImagePull(string $id) : void { + $container = $this->containerDefinitionFetcher->GetContainerById($id); + + // Pull all dependencies first and then itself + foreach($container->dependsOn as $dependency) { + $this->PerformRecursiveImagePull($dependency); + } + + $this->dockerActionManager->PullImage($container, true); + } + + public function PullAllContainerImages(): void { + + $id = self::TOP_CONTAINER; + + $this->PerformRecursiveImagePull($id); + } + + public function GetLogs(Request $request, Response $response, array $args) : Response { - $id = $request->getQueryParams()['id']; + $requestParams = $request->getQueryParams(); + $id = ''; + if (isset($requestParams['id']) && is_string($requestParams['id'])) { + $id = $requestParams['id']; + } if (str_starts_with($id, 'nextcloud-aio-')) { $logs = $this->dockerActionManager->GetLogs($id); } else { @@ -68,39 +82,80 @@ class DockerController ->withHeader('Content-Disposition', 'inline'); } - public function StartBackupContainerBackup(Request $request, Response $response, $args) : Response { - $this->startBackup(); - return $response->withStatus(201)->withHeader('Location', '/'); + public function StartBackupContainerBackup(Request $request, Response $response, array $args) : Response { + $forceStopNextcloud = true; + $this->startBackup($forceStopNextcloud); + return $response->withStatus(201)->withHeader('Location', '.'); } - public function startBackup() : void { - $config = $this->configurationManager->GetConfig(); - $config['backup-mode'] = 'backup'; - $this->configurationManager->WriteConfig($config); + public function startBackup(bool $forceStopNextcloud = false) : void { + $this->configurationManager->backupMode = 'backup'; $id = self::TOP_CONTAINER; - $this->PerformRecursiveContainerStop($id); + $this->PerformRecursiveContainerStop($id, $forceStopNextcloud); $id = 'nextcloud-aio-borgbackup'; $this->PerformRecursiveContainerStart($id); } - public function StartBackupContainerCheck(Request $request, Response $response, $args) : Response { - $config = $this->configurationManager->GetConfig(); - $config['backup-mode'] = 'check'; - $this->configurationManager->WriteConfig($config); + public function StartBackupContainerCheck(Request $request, Response $response, array $args) : Response { + $this->checkBackup(); + return $response->withStatus(201)->withHeader('Location', '.'); + } + + public function StartBackupContainerList(Request $request, Response $response, array $args) : Response { + $this->listBackup(); + return $response->withStatus(201)->withHeader('Location', '.'); + } + + public function checkBackup() : void { + $this->configurationManager->backupMode = 'check'; + + $id = 'nextcloud-aio-borgbackup'; + $this->PerformRecursiveContainerStart($id); + } + + private function listBackup() : void { + $this->configurationManager->backupMode = 'list'; + + $id = 'nextcloud-aio-borgbackup'; + $this->PerformRecursiveContainerStart($id); + } + + public function StartBackupContainerRestore(Request $request, Response $response, array $args) : Response { + $this->configurationManager->startTransaction(); + $this->configurationManager->backupMode = 'restore'; + $this->configurationManager->selectedRestoreTime = $request->getParsedBody()['selected_restore_time'] ?? ''; + $this->configurationManager->restoreExcludePreviews = isset($request->getParsedBody()['restore-exclude-previews']); + $this->configurationManager->commitTransaction(); + + $id = self::TOP_CONTAINER; + $forceStopNextcloud = true; + $this->PerformRecursiveContainerStop($id, $forceStopNextcloud); $id = 'nextcloud-aio-borgbackup'; $this->PerformRecursiveContainerStart($id); - return $response->withStatus(201)->withHeader('Location', '/'); + return $response->withStatus(201)->withHeader('Location', '.'); } - public function StartBackupContainerRestore(Request $request, Response $response, $args) : Response { - $config = $this->configurationManager->GetConfig(); - $config['backup-mode'] = 'restore'; - $config['selected-restore-time'] = $request->getParsedBody()['selected_restore_time']; - $this->configurationManager->WriteConfig($config); + public function StartBackupContainerCheckRepair(Request $request, Response $response, array $args) : Response { + $this->configurationManager->backupMode = 'check-repair'; + + $id = 'nextcloud-aio-borgbackup'; + $this->PerformRecursiveContainerStart($id); + + // Restore to backup check which is needed to make the UI logic work correctly + $this->configurationManager->backupMode = 'check'; + + return $response->withStatus(201)->withHeader('Location', '.'); + } + + public function StartBackupContainerTest(Request $request, Response $response, array $args) : Response { + $this->configurationManager->startTransaction(); + $this->configurationManager->backupMode = 'test'; + $this->configurationManager->instanceRestoreAttempt = false; + $this->configurationManager->commitTransaction(); $id = self::TOP_CONTAINER; $this->PerformRecursiveContainerStop($id); @@ -108,64 +163,64 @@ class DockerController $id = 'nextcloud-aio-borgbackup'; $this->PerformRecursiveContainerStart($id); - return $response->withStatus(201)->withHeader('Location', '/'); + return $response->withStatus(201)->withHeader('Location', '.'); } - public function StartBackupContainerTest(Request $request, Response $response, $args) : Response { - $config = $this->configurationManager->GetConfig(); - $config['backup-mode'] = 'test'; - $config['instance_restore_attempt'] = 0; - $this->configurationManager->WriteConfig($config); - - $id = self::TOP_CONTAINER; - $this->PerformRecursiveContainerStop($id); - - $id = 'nextcloud-aio-borgbackup'; - $this->PerformRecursiveContainerStart($id); - - return $response->withStatus(201)->withHeader('Location', '/'); - } - - public function StartContainer(Request $request, Response $response, $args) : Response + public function StartContainer(Request $request, Response $response, array $args) : Response { $uri = $request->getUri(); $host = $uri->getHost(); $port = $uri->getPort(); + $path = $request->getParsedBody()['base_path'] ?? ''; if ($port === 8000) { error_log('The AIO_URL-port was discovered to be 8000 which is not expected. It is now set to 443.'); $port = 443; } - $config = $this->configurationManager->GetConfig(); + if (isset($request->getParsedBody()['install_latest_major'])) { + $installLatestMajor = '32'; + } else { + $installLatestMajor = ''; + } + + $this->configurationManager->startTransaction(); + $this->configurationManager->installLatestMajor = $installLatestMajor; // set AIO_URL - $config['AIO_URL'] = $host . ':' . $port; + $this->configurationManager->aioUrl = $host . ':' . (string)$port . $path; // set wasStartButtonClicked - $config['wasStartButtonClicked'] = 1; - $this->configurationManager->WriteConfig($config); - + $this->configurationManager->wasStartButtonClicked = true; + $this->configurationManager->commitTransaction(); + + // Do not pull container images in case 'bypass_container_update' is set via url params + // Needed for local testing + $pullImage = !isset($request->getParsedBody()['bypass_container_update']); + if ($pullImage === false) { + error_log('WARNING: Not pulling container images. Instead, using local ones.'); + } // Start container - $this->startTopContainer(true); + $this->startTopContainer($pullImage); - return $response->withStatus(201)->withHeader('Location', '/'); + // Clear apcu cache in order to check if container updates are available + // Temporarily disabled as it leads much faster to docker rate limits + // apcu_clear_cache(); + + return $response->withStatus(201)->withHeader('Location', '.'); } - public function startTopContainer(bool $pullContainer) : void { - $config = $this->configurationManager->GetConfig(); - // set AIO_TOKEN - $config['AIO_TOKEN'] = bin2hex(random_bytes(24)); - $this->configurationManager->WriteConfig($config); + public function startTopContainer(bool $pullImage) : void { + $this->configurationManager->aioToken = bin2hex(random_bytes(24)); // Stop domaincheck since apache would not be able to start otherwise $this->StopDomaincheckContainer(); $id = self::TOP_CONTAINER; - $this->PerformRecursiveContainerStart($id, $pullContainer); + $this->PerformRecursiveContainerStart($id, $pullImage); } - public function StartWatchtowerContainer(Request $request, Response $response, $args) : Response { + public function StartWatchtowerContainer(Request $request, Response $response, array $args) : Response { $this->startWatchtower(); - return $response->withStatus(201)->withHeader('Location', '/'); + return $response->withStatus(201)->withHeader('Location', '.'); } public function startWatchtower() : void { @@ -174,24 +229,36 @@ class DockerController $this->PerformRecursiveContainerStart($id); } - private function PerformRecursiveContainerStop(string $id) : void + private function PerformRecursiveContainerStop(string $id, bool $forceStopNextcloud = false) : void { $container = $this->containerDefinitionFetcher->GetContainerById($id); - foreach($container->GetDependsOn() as $dependency) { - $this->PerformRecursiveContainerStop($dependency); + + // This is a hack but no better solution was found for the meantime + // Stop Collabora first to make sure it force-saves + // See https://github.com/nextcloud/richdocuments/issues/3799 + if ($id === self::TOP_CONTAINER && $this->configurationManager->isCollaboraEnabled) { + $this->PerformRecursiveContainerStop('nextcloud-aio-collabora'); } - // Disconnecting is not needed. This also allows to start the containers manually via docker-cli - //$this->dockerActionManager->DisconnectContainerFromNetwork($container); - $this->dockerActionManager->StopContainer($container); + // Stop itself first and then all the dependencies + if ($id !== 'nextcloud-aio-nextcloud') { + $this->dockerActionManager->StopContainer($container); + } else { + // We want to stop the Nextcloud container after 10s and not wait for the configured stop_grace_period + $this->dockerActionManager->StopContainer($container, $forceStopNextcloud); + } + foreach($container->dependsOn as $dependency) { + $this->PerformRecursiveContainerStop($dependency, $forceStopNextcloud); + } } - public function StopContainer(Request $request, Response $response, $args) : Response + public function StopContainer(Request $request, Response $response, array $args) : Response { $id = self::TOP_CONTAINER; - $this->PerformRecursiveContainerStop($id); + $forceStopNextcloud = true; + $this->PerformRecursiveContainerStop($id, $forceStopNextcloud); - return $response->withStatus(201)->withHeader('Location', '/'); + return $response->withStatus(201)->withHeader('Location', '.'); } public function stopTopContainer() : void { @@ -202,7 +269,7 @@ class DockerController public function StartDomaincheckContainer() : void { # Don't start if domain is already set - if ($this->configurationManager->GetDomain() !== '' || $this->configurationManager->wasStartButtonClicked()) { + if ($this->configurationManager->domain !== '' || $this->configurationManager->wasStartButtonClicked) { return; } @@ -213,10 +280,10 @@ class DockerController $domaincheckContainer = $this->containerDefinitionFetcher->GetContainerById($id); $apacheContainer = $this->containerDefinitionFetcher->GetContainerById(self::TOP_CONTAINER); // Don't start if apache is already running - if ($apacheContainer->GetRunningState() instanceof RunningState) { + if ($apacheContainer->GetRunningState() === ContainerState::Running) { return; // Don't start if domaincheck is already running - } elseif ($domaincheckContainer->GetRunningState() instanceof RunningState) { + } elseif ($domaincheckContainer->GetRunningState() === ContainerState::Running) { $domaincheckWasStarted = apcu_fetch($cacheKey); // Start domaincheck again when 10 minutes are over by not returning here if($domaincheckWasStarted !== false && is_string($domaincheckWasStarted)) { @@ -225,7 +292,11 @@ class DockerController } $this->StopDomaincheckContainer(); - $this->PerformRecursiveContainerStart($id); + try { + $this->PerformRecursiveContainerStart($id); + } catch (\Exception $e) { + error_log('Could not start domaincheck container: ' . $e->getMessage()); + } // Cache the start for 10 minutes apcu_add($cacheKey, '1', 600); diff --git a/php/src/Controller/LoginController.php b/php/src/Controller/LoginController.php index aaab2952..412ff9df 100644 --- a/php/src/Controller/LoginController.php +++ b/php/src/Controller/LoginController.php @@ -9,44 +9,43 @@ use AIO\Docker\DockerActionManager; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; -class LoginController -{ - private AuthManager $authManager; - private DockerActionManager $dockerActionManager; - - public function __construct(AuthManager $authManager, DockerActionManager $dockerActionManager) { - $this->authManager = $authManager; - $this->dockerActionManager = $dockerActionManager; +readonly class LoginController { + public function __construct( + private AuthManager $authManager, + private DockerActionManager $dockerActionManager, + ) { } - public function TryLogin(Request $request, Response $response, $args) : Response { + public function TryLogin(Request $request, Response $response, array $args) : Response { if (!$this->dockerActionManager->isLoginAllowed()) { - return $response->withHeader('Location', '/')->withStatus(302); + $response->getBody()->write("The login is blocked since Nextcloud is running."); + return $response->withHeader('Location', '.')->withStatus(422); } - $password = $request->getParsedBody()['password']; + $password = $request->getParsedBody()['password'] ?? ''; if($this->authManager->CheckCredentials($password)) { $this->authManager->SetAuthState(true); - return $response->withHeader('Location', '/')->withStatus(302); + return $response->withHeader('Location', '.')->withStatus(201); } - return $response->withHeader('Location', '/')->withStatus(302); + $response->getBody()->write("The password is incorrect."); + return $response->withHeader('Location', '.')->withStatus(422); } - public function GetTryLogin(Request $request, Response $response, $args) : Response { - $token = $request->getQueryParams()['token']; + public function GetTryLogin(Request $request, Response $response, array $args) : Response { + $token = $request->getQueryParams()['token'] ?? ''; if($this->authManager->CheckToken($token)) { $this->authManager->SetAuthState(true); - return $response->withHeader('Location', '/')->withStatus(302); + return $response->withHeader('Location', '../..')->withStatus(302); } - return $response->withHeader('Location', '/')->withStatus(302); + return $response->withHeader('Location', '../..')->withStatus(302); } - public function Logout(Request $request, Response $response, $args) : Response + public function Logout(Request $request, Response $response, array $args) : Response { $this->authManager->SetAuthState(false); return $response - ->withHeader('Location', '/') + ->withHeader('Location', '../..') ->withStatus(302); } } diff --git a/php/src/Cron/BackupNotification.php b/php/src/Cron/BackupNotification.php index a570031b..6fbab65f 100644 --- a/php/src/Cron/BackupNotification.php +++ b/php/src/Cron/BackupNotification.php @@ -1,29 +1,33 @@ -get(\AIO\Docker\DockerActionManager::class); -/** @var \AIO\ContainerDefinitionFetcher $containerDefinitionFetcher */ -$containerDefinitionFetcher = $container->get(\AIO\ContainerDefinitionFetcher::class); - -$id = 'nextcloud-aio-nextcloud'; -$nextcloudContainer = $containerDefinitionFetcher->GetContainerById($id); - -$backupExitCode = $dockerActionManger->GetBackupcontainerExitCode(); - -if ($backupExitCode === 0) { - $dockerActionManger->sendNotification($nextcloudContainer, 'Daily backup successful!', 'You can get further info by looking at the backup logs in the AIO interface.'); -} - -if ($backupExitCode > 0) { - $dockerActionManger->sendNotification($nextcloudContainer, 'Daily backup failed!', 'You can get further info by looking at the backup logs in the AIO interface.'); -} +get(\AIO\Docker\DockerActionManager::class); +/** @var \AIO\ContainerDefinitionFetcher $containerDefinitionFetcher */ +$containerDefinitionFetcher = $container->get(\AIO\ContainerDefinitionFetcher::class); + +$id = 'nextcloud-aio-nextcloud'; +$nextcloudContainer = $containerDefinitionFetcher->GetContainerById($id); + +$backupExitCode = $dockerActionManager->GetBackupcontainerExitCode(); + +if ($backupExitCode === 0) { + if (getenv('SEND_SUCCESS_NOTIFICATIONS') === "0") { + error_log("Daily backup successful! Only logging successful backup and not sending backup notification since that has been disabled! You can get further info by looking at the backup logs in the AIO interface."); + } else { + $dockerActionManager->sendNotification($nextcloudContainer, 'Daily backup successful!', 'You can get further info by looking at the backup logs in the AIO interface.'); + } +} + +if ($backupExitCode > 0) { + $dockerActionManager->sendNotification($nextcloudContainer, 'Daily backup failed!', 'You can get further info by looking at the backup logs in the AIO interface.'); +} diff --git a/php/src/Cron/CheckBackup.php b/php/src/Cron/CheckBackup.php new file mode 100644 index 00000000..6d9b027c --- /dev/null +++ b/php/src/Cron/CheckBackup.php @@ -0,0 +1,17 @@ +get(\AIO\Controller\DockerController::class); + +// Stop container and start backup check +$dockerController->checkBackup(); diff --git a/php/src/Cron/CheckFreeDiskSpace.php b/php/src/Cron/CheckFreeDiskSpace.php new file mode 100644 index 00000000..1b5d2d64 --- /dev/null +++ b/php/src/Cron/CheckFreeDiskSpace.php @@ -0,0 +1,26 @@ +get(\AIO\Docker\DockerActionManager::class); +/** @var \AIO\ContainerDefinitionFetcher $containerDefinitionFetcher */ +$containerDefinitionFetcher = $container->get(\AIO\ContainerDefinitionFetcher::class); + +$id = 'nextcloud-aio-nextcloud'; +$nextcloudContainer = $containerDefinitionFetcher->GetContainerById($id); + +$df = disk_free_space(DataConst::GetDataDirectory()); +if ($df !== false && (int)$df < 1024 * 1024 * 1024 * 5) { + error_log("The drive that hosts the mastercontainer volume has less than 5 GB free space. Container updates and backups might not succeed due to that!"); + $dockerActionManager->sendNotification($nextcloudContainer, 'Low on space!', 'The drive that hosts the mastercontainer volume has less than 5 GB free space. Container updates and backups might not succeed due to that!'); +} diff --git a/php/src/Cron/OutdatedNotification.php b/php/src/Cron/OutdatedNotification.php new file mode 100644 index 00000000..628f0924 --- /dev/null +++ b/php/src/Cron/OutdatedNotification.php @@ -0,0 +1,26 @@ +get(\AIO\Docker\DockerActionManager::class); +/** @var \AIO\ContainerDefinitionFetcher $containerDefinitionFetcher */ +$containerDefinitionFetcher = $container->get(\AIO\ContainerDefinitionFetcher::class); + +$id = 'nextcloud-aio-nextcloud'; +$nextcloudContainer = $containerDefinitionFetcher->GetContainerById($id); + +$isNextcloudImageOutdated = $dockerActionManager->isNextcloudImageOutdated(); + +if ($isNextcloudImageOutdated === true) { + $dockerActionManager->sendNotification($nextcloudContainer, 'AIO is outdated!', 'Please open the AIO interface or ask an administrator to update it. If you do not want to do it manually each time, you can enable the daily backup feature from the AIO interface which automatically updates all containers.', '/notify-all.sh'); +} + diff --git a/php/src/Cron/PullContainerImages.php b/php/src/Cron/PullContainerImages.php new file mode 100644 index 00000000..43c87d28 --- /dev/null +++ b/php/src/Cron/PullContainerImages.php @@ -0,0 +1,20 @@ +get(\AIO\Controller\DockerController::class); + +// Pull all containers +$dockerController->PullAllContainerImages(); diff --git a/php/src/Cron/StartAndUpdateContainers.php b/php/src/Cron/StartAndUpdateContainers.php index e72ae804..049245af 100644 --- a/php/src/Cron/StartAndUpdateContainers.php +++ b/php/src/Cron/StartAndUpdateContainers.php @@ -4,6 +4,9 @@ declare(strict_types=1); // increase memory limit to 2GB ini_set('memory_limit', '2048M'); +// Log whole log messages +ini_set('log_errors_max_len', '0'); + use DI\Container; require __DIR__ . '/../../vendor/autoload.php'; diff --git a/php/src/Cron/StartContainers.php b/php/src/Cron/StartContainers.php index 98f9ae8f..366866ad 100644 --- a/php/src/Cron/StartContainers.php +++ b/php/src/Cron/StartContainers.php @@ -4,6 +4,9 @@ declare(strict_types=1); // increase memory limit to 2GB ini_set('memory_limit', '2048M'); +// Log whole log messages +ini_set('log_errors_max_len', '0'); + use DI\Container; require __DIR__ . '/../../vendor/autoload.php'; diff --git a/php/src/Cron/UpdateNotification.php b/php/src/Cron/UpdateNotification.php index e1d57f6a..2c12e2f4 100644 --- a/php/src/Cron/UpdateNotification.php +++ b/php/src/Cron/UpdateNotification.php @@ -10,21 +10,21 @@ require __DIR__ . '/../../vendor/autoload.php'; $container = \AIO\DependencyInjection::GetContainer(); -/** @var \AIO\Docker\DockerActionManager $dockerActionManger */ -$dockerActionManger = $container->get(\AIO\Docker\DockerActionManager::class); +/** @var \AIO\Docker\DockerActionManager $dockerActionManager */ +$dockerActionManager = $container->get(\AIO\Docker\DockerActionManager::class); /** @var \AIO\ContainerDefinitionFetcher $containerDefinitionFetcher */ $containerDefinitionFetcher = $container->get(\AIO\ContainerDefinitionFetcher::class); $id = 'nextcloud-aio-nextcloud'; $nextcloudContainer = $containerDefinitionFetcher->GetContainerById($id); -$isMastercontainerUpdateAvailable = $dockerActionManger->IsMastercontainerUpdateAvailable(); -$isAnyUpdateAvailable = $dockerActionManger->isAnyUpdateAvailable(); +$isMastercontainerUpdateAvailable = $dockerActionManager->IsMastercontainerUpdateAvailable(); +$isAnyUpdateAvailable = $dockerActionManager->isAnyUpdateAvailable(); if ($isMastercontainerUpdateAvailable === true) { - $dockerActionManger->sendNotification($nextcloudContainer, 'Mastercontainer update available!', 'Please open your AIO interface to update it. If you do not want to do it manually each time, you can enable the daily backup feature from the AIO interface which also automatically updates the mastercontainer.'); + $dockerActionManager->sendNotification($nextcloudContainer, 'Mastercontainer update available!', 'Please open your AIO interface to update it. If you do not want to do it manually each time, you can enable the daily backup feature from the AIO interface which also automatically updates the mastercontainer.'); } if ($isAnyUpdateAvailable === true) { - $dockerActionManger->sendNotification($nextcloudContainer, 'Container updates available!', 'Please open your AIO interface to update them. If you do not want to do it manually each time, you can enable the daily backup feature from the AIO interface which also automatically updates your containers and your Nextcloud apps.'); + $dockerActionManager->sendNotification($nextcloudContainer, 'Container updates available!', 'Please open your AIO interface to update them. If you do not want to do it manually each time, you can enable the daily backup feature from the AIO interface which also automatically updates your containers and your Nextcloud apps.'); } diff --git a/php/src/Data/ConfigurationManager.php b/php/src/Data/ConfigurationManager.php index 2ebcf53e..7534acda 100644 --- a/php/src/Data/ConfigurationManager.php +++ b/php/src/Data/ConfigurationManager.php @@ -7,46 +7,356 @@ use AIO\Controller\DockerController; class ConfigurationManager { - public function GetConfig() : array + private array $secrets = []; + + private array $config = []; + + private bool $noWrite = false; + + public string $aioToken { + get => $this->get('AIO_TOKEN', ''); + set { $this->set('AIO_TOKEN', $value); } + } + + public string $password { + get => $this->get('password', ''); + set { $this->set('password', $value); } + } + + public bool $isDockerSocketProxyEnabled { + // Type-cast because old configs could have 1/0 for this key. + get => (bool) $this->get('isDockerSocketProxyEnabled', false); + set { $this->set('isDockerSocketProxyEnabled', $value); } + } + + public bool $isWhiteboardEnabled { + // Type-cast because old configs could have 1/0 for this key. + get => (bool) $this->get('isWhiteboardEnabled', true); + set { $this->set('isWhiteboardEnabled', $value); } + } + + public bool $restoreExcludePreviews { + // Type-cast because old configs could have '1'/'' for this key. + get => (bool) $this->get('restore-exclude-previews', false); + set { $this->set('restore-exclude-previews', $value); } + } + + public string $selectedRestoreTime { + get => $this->get('selected-restore-time', ''); + set { $this->set('selected-restore-time', $value); } + } + + public string $backupMode { + get => $this->get('backup-mode', ''); + set { $this->set('backup-mode', $value); } + } + + public bool $instanceRestoreAttempt { + // Type-cast because old configs could have 1/'' for this key. + get => (bool) $this->get('instance_restore_attempt', false); + set { $this->set('instance_restore_attempt', $value); } + } + + public string $aioUrl { + get => $this->get('AIO_URL', ''); + set { $this->set('AIO_URL', $value); } + } + + public bool $wasStartButtonClicked { + // Type-cast because old configs could have 1/0 for this key. + get => (bool) $this->get('wasStartButtonClicked', false); + set { $this->set('wasStartButtonClicked', $value); } + } + + public string $installLatestMajor { + // Type-cast because old configs could have integers for this key. + get => (string) $this->get('install_latest_major', ''); + set { $this->set('install_latest_major', $value); } + } + + public bool $isClamavEnabled { + // Type-cast because old configs could have 1/0 for this key. + get => (bool) $this->get('isClamavEnabled', false); + set { $this->set('isClamavEnabled', $value); } + } + + public bool $isOnlyofficeEnabled { + // Type-cast because old configs could have 1/0 for this key. + get => (bool) $this->get('isOnlyofficeEnabled', false); + set { $this->set('isOnlyofficeEnabled', $value); } + } + + public bool $isCollaboraEnabled { + // Type-cast because old configs could have 1/0 for this key. + get => (bool) $this->get('isCollaboraEnabled', true); + set { $this->set('isCollaboraEnabled', $value); } + } + + public bool $isTalkEnabled { + // Type-cast because old configs could have 1/0 for this key. + get => (bool) $this->get('isTalkEnabled', true); + set { $this->set('isTalkEnabled', $value); } + } + + public bool $isTalkRecordingEnabled { + // Type-cast because old configs could have 1/0 for this key. + get => (bool) $this->isTalkEnabled && $this->get('isTalkRecordingEnabled', false); + set { $this->set('isTalkRecordingEnabled', $this->isTalkEnabled && $value); } + } + + public bool $isImaginaryEnabled { + // Type-cast because old configs could have 1/0 for this key. + get => (bool) $this->get('isImaginaryEnabled', true); + set { $this->set('isImaginaryEnabled', $value); } + } + + public bool $isFulltextsearchEnabled { + // Type-cast because old configs could have 1/0 for this key. + get => (bool) $this->get('isFulltextsearchEnabled', false); + // Elasticsearch does not work on kernels without seccomp anymore. See https://github.com/nextcloud/all-in-one/discussions/5768 + set { $this->set('isFulltextsearchEnabled', ($this->collaboraSeccompDisabled && $value)); } + } + + public string $domain { + get => $this->get('domain', ''); + set { $this->setDomain($value); } + } + + public string $borgBackupHostLocation { + get => $this->get('borg_backup_host_location', ''); + set { $this->set('borg_backup_host_location', $value); } + } + + public string $borgRemoteRepo { + get => $this->get('borg_remote_repo', ''); + set { $this->set('borg_remote_repo', $value); } + } + + public string $borgRestorePassword { + get => $this->get('borg_restore_password', ''); + set { $this->set('borg_restore_password', $value); } + } + + public string $apacheIpBinding { + get => $this->getEnvironmentalVariableOrConfig('APACHE_IP_BINDING', 'apache_ip_binding', ''); + set { $this->set('apache_ip_binding', $value); } + } + + /** + * @throws InvalidSettingConfigurationException + */ + public string $timezone { + get => $this->get('timezone', ''); + set { + // This throws an exception if the validation fails. + $this->validateTimezone($value); + $this->set('timezone', $value); + } + } + + /** + * @throws InvalidSettingConfigurationException + */ + public string $collaboraDictionaries { + get => $this->get('collabora_dictionaries', ''); + set { + // This throws an exception if the validation fails. + $this->validateCollaboraDictionaries($value); + $this->set('collabora_dictionaries', $value); + } + } + + /** + * @throws InvalidSettingConfigurationException + */ + public string $collaboraAdditionalOptions { + get => $this->get('collabora_additional_options', ''); + set { + // This throws an exception if the validation fails. + $this->validateCollaboraAdditionalOptions($value); + $this->set('collabora_additional_options', $value); + } + } + + public array $aioCommunityContainers { + get => explode(' ', $this->get('aio_community_containers', '')); + set { $this->set('aio_community_containers', implode(' ', $value)); } + } + + public string $turnDomain { + get => $this->get('turn_domain', ''); + set { $this->set('turn_domain', $value); } + } + + public string $apachePort { + get => $this->getEnvironmentalVariableOrConfig('APACHE_PORT', 'apache_port', '443'); + set { $this->set('apache_port', $value); } + } + + public string $talkPort { + get => $this->getEnvironmentalVariableOrConfig('TALK_PORT', 'talk_port', '3478'); + set { $this->set('talk_port', $value); } + } + + public string $nextcloudMount { + get => $this->getEnvironmentalVariableOrConfig('NEXTCLOUD_MOUNT', 'nextcloud_mount', ''); + set { $this->set('nextcloud_mount', $value); } + } + + public string $nextcloudDatadirMount { + get => $this->getEnvironmentalVariableOrConfig('NEXTCLOUD_DATADIR', 'nextcloud_datadir', 'nextcloud_aio_nextcloud_data'); + set { $this->set('nextcloud_datadir_mount', $value); } + } + + public string $nextcloudUploadLimit { + get => $this->getEnvironmentalVariableOrConfig('NEXTCLOUD_UPLOAD_LIMIT', 'nextcloud_upload_limit', '16G'); + set { $this->set('nextcloud_upload_limit', $value); } + } + + public string $nextcloudMemoryLimit { + get => $this->getEnvironmentalVariableOrConfig('NEXTCLOUD_MEMORY_LIMIT', 'nextcloud_memory_limit', '512M'); + set { $this->set('nextcloud_memory_limit', $value); } + } + + public function getApacheMaxSize() : int { + $uploadLimit = (int)rtrim($this->nextcloudUploadLimit, 'G'); + return $uploadLimit * 1024 * 1024 * 1024; + } + + public string $nextcloudMaxTime { + get => $this->getEnvironmentalVariableOrConfig('NEXTCLOUD_MAX_TIME', 'nextcloud_max_time', '3600'); + set { $this->set('nextcloud_max_time', $value); } + } + + public string $borgRetentionPolicy { + get => $this->getEnvironmentalVariableOrConfig('BORG_RETENTION_POLICY', 'borg_retention_policy', '--keep-within=7d --keep-weekly=4 --keep-monthly=6'); + set { $this->set('borg_retention_policy', $value); } + } + + public string $fulltextsearchJavaOptions { + get => $this->getEnvironmentalVariableOrConfig('FULLTEXTSEARCH_JAVA_OPTIONS', 'fulltextsearch_java_options', '-Xms512M -Xmx512M'); + set { $this->set('fulltextsearch_java_options', $value); } + } + + public string $dockerSocketPath { + get => $this->getEnvironmentalVariableOrConfig('WATCHTOWER_DOCKER_SOCKET_PATH', 'docker_socket_path', '/var/run/docker.sock'); + set { $this->set('docker_socket_path', $value); } + } + + public string $trustedCacertsDir { + get => $this->getEnvironmentalVariableOrConfig('NEXTCLOUD_TRUSTED_CACERTS_DIR', 'trusted_cacerts_dir', ''); + set { $this->set('trusted_cacerts_dir', $value); } + } + + public string $nextcloudAdditionalApks { + get => trim($this->getEnvironmentalVariableOrConfig('NEXTCLOUD_ADDITIONAL_APKS', 'nextcloud_additional_apks', 'imagemagick')); + set { $this->set('nextcloud_addtional_apks', $value); } + } + + public string $nextcloudAdditionalPhpExtensions { + get => trim($this->getEnvironmentalVariableOrConfig('NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS', 'nextcloud_additional_php_extensions', 'imagick')); + set { $this->set('nextcloud_additional_php_extensions', $value); } + } + + public bool $collaboraSeccompDisabled { + get => $this->booleanize($this->getEnvironmentalVariableOrConfig('COLLABORA_SECCOMP_DISABLED', 'collabora_seccomp_disabled', '')); + set { $this->set('collabora_seccomp_disabled', $value); } + } + + public bool $disableBackupSection { + get => $this->booleanize($this->getEnvironmentalVariableOrConfig('AIO_DISABLE_BACKUP_SECTION', 'disable_backup_section', '')); + set { $this->set('disable_backup_section', $value); } + } + + public bool $nextcloudEnableDriDevice{ + get => $this->booleanize($this->getEnvironmentalVariableOrConfig('NEXTCLOUD_ENABLE_DRI_DEVICE', 'nextcloud_enable_dri_device', '')); + set { $this->set('nextcloud_enable_dri_device', $value); } + } + + public bool $enableNvidiaGpu { + get => $this->booleanize($this->getEnvironmentalVariableOrConfig('NEXTCLOUD_ENABLE_NVIDIA_GPU', 'enable_nvidia_gpu', '')); + set { $this->set('enable_nvidia_gpu', $value); } + } + + public bool $nextcloudKeepDisabledApps { + get => $this->booleanize($this->getEnvironmentalVariableOrConfig('NEXTCLOUD_KEEP_DISABLED_APPS', 'nextcloud_keep_disabled_apps', '')); + set { $this->set('nextcloud_keep_disabled_apps', $value); } + } + + private function getConfig() : array { - if(file_exists(DataConst::GetConfigFile())) + if ($this->config === [] && file_exists(DataConst::GetConfigFile())) { - $configContent = file_get_contents(DataConst::GetConfigFile()); - return json_decode($configContent, true); + $configContent = (string)file_get_contents(DataConst::GetConfigFile()); + $this->config = json_decode($configContent, true, 512, JSON_THROW_ON_ERROR); } - return []; + return $this->config; } - public function GetPassword() : string { - return $this->GetConfig()['password']; + private function get(string $key, mixed $fallbackValue = null) : mixed { + return $this->getConfig()[$key] ?? $fallbackValue; } - public function GetToken() : string { - return $this->GetConfig()['AIO_TOKEN']; + private function set(string $key, mixed $value) : void { + $this->getConfig(); + $this->config[$key] = $value; + // Only write if this isn't called in between startTransaction() and commitTransaction(). + if ($this->noWrite !== true) { + $this->writeConfig(); + } } - public function SetPassword(string $password) : void { - $config = $this->GetConfig(); - $config['password'] = $password; - $this->WriteConfig($config); + /** + * This allows to assign multiple attributes without saving the config to disk in between. It must be + * followed by a call to commitTransaction(), which then writes all changes to disk. + */ + public function startTransaction() : void { + $this->getConfig(); + $this->noWrite = true; } - public function GetSecret(string $secretId) : string { - $config = $this->GetConfig(); - if(!isset($config['secrets'][$secretId])) { - $config['secrets'][$secretId] = bin2hex(random_bytes(24)); - $this->WriteConfig($config); + /** + * This allows to assign multiple attributes without saving the config to disk in between. + */ + public function commitTransaction() : void { + try { + $this->writeConfig(); + } finally { + $this->noWrite = false; + } + } + + public function getAndGenerateSecret(string $secretId) : string { + if ($secretId === '') { + return ''; + } + + $secrets = $this->get('secrets', []); + if (!isset($secrets[$secretId])) { + $secrets[$secretId] = bin2hex(random_bytes(24)); + $this->set('secrets', $secrets); } if ($secretId === 'BORGBACKUP_PASSWORD' && !file_exists(DataConst::GetBackupSecretFile())) { - $this->DoubleSafeBackupSecret($config['secrets'][$secretId]); + $this->doubleSafeBackupSecret($secrets[$secretId]); } - return $config['secrets'][$secretId]; + return $secrets[$secretId]; } - private function DoubleSafeBackupSecret(string $borgBackupPassword) : void { + public function getRegisteredSecret(string $secretId) : string { + if ($this->secrets[$secretId]) { + return $this->getAndGenerateSecret($secretId); + } + throw new \Exception("The secret " . $secretId . " was not registered. Please check if it is defined in secrets of containers.json."); + } + + public function registerSecret(string $secretId) : void { + $this->secrets[$secretId] = true; + } + + private function doubleSafeBackupSecret(string $borgBackupPassword) : void { file_put_contents(DataConst::GetBackupSecretFile(), $borgBackupPassword); } @@ -58,18 +368,18 @@ class ConfigurationManager } } - public function GetLastBackupTime() : string { + public function getLastBackupTime() : string { if (!file_exists(DataConst::GetBackupArchivesList())) { return ''; } - - $content = file_get_contents(DataConst::GetBackupArchivesList()); - if ($content === '') { - return ''; - } + + $content = (string)file_get_contents(DataConst::GetBackupArchivesList()); $lastBackupLines = explode("\n", $content); - $lastBackupLine = $lastBackupLines[sizeof($lastBackupLines) - 2]; + $lastBackupLine = ""; + if (count($lastBackupLines) >= 2) { + $lastBackupLine = $lastBackupLines[sizeof($lastBackupLines) - 2]; + } if ($lastBackupLine === "") { return ''; } @@ -79,26 +389,23 @@ class ConfigurationManager if ($lastBackupTime === "") { return ''; } - + return $lastBackupTime; } - public function GetBackupTimes() : array { + public function getBackupTimes() : array { if (!file_exists(DataConst::GetBackupArchivesList())) { return []; } - - $content = file_get_contents(DataConst::GetBackupArchivesList()); - if ($content === '') { - return []; - } + + $content = (string)file_get_contents(DataConst::GetBackupArchivesList()); $backupLines = explode("\n", $content); $backupTimes = []; foreach($backupLines as $lines) { if ($lines !== "") { $backupTimesTemp = explode(',', $lines); - $backupTimes[] = $backupTimesTemp[1]; + $backupTimes[] = $backupTimesTemp[1]; } } @@ -108,15 +415,15 @@ class ConfigurationManager return $backupTimes; } - public function wasStartButtonClicked() : bool { - if (isset($this->GetConfig()['wasStartButtonClicked'])) { - return true; - } else { - return false; + public function getAioVersion() : string { + $path = DataConst::GetAioVersionFile(); + if ($path !== '' && file_exists($path)) { + return trim((string)file_get_contents($path)); } + return ''; } - public function isx64Platform() : bool { + private function isx64Platform() : bool { if (php_uname('m') === 'x86_64') { return true; } else { @@ -124,102 +431,29 @@ class ConfigurationManager } } - public function isClamavEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isClamavEnabled']) && $config['isClamavEnabled'] === 1) { - return true; - } else { - return false; - } - } - - public function SetClamavEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isClamavEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isImaginaryEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isImaginaryEnabled']) && $config['isImaginaryEnabled'] === 1) { - return true; - } else { - return false; - } - } - - public function SetImaginaryEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isImaginaryEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isFulltextsearchEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isFulltextsearchEnabled']) && $config['isFulltextsearchEnabled'] === 1) { - return true; - } else { - return false; - } - } - - public function SetFulltextsearchEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isFulltextsearchEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isOnlyofficeEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isOnlyofficeEnabled']) && $config['isOnlyofficeEnabled'] === 1) { - return true; - } else { - return false; - } - } - - public function SetOnlyofficeEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isOnlyofficeEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isCollaboraEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isCollaboraEnabled']) && $config['isCollaboraEnabled'] === 0) { - return false; - } else { - return true; - } - } - - public function SetCollaboraEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isCollaboraEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isTalkEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isTalkEnabled']) && $config['isTalkEnabled'] === 0) { - return false; - } else { - return true; - } - } - - public function SetTalkEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isTalkEnabled'] = $value; - $this->WriteConfig($config); - } - /** * @throws InvalidSettingConfigurationException + * + * We can't turn this into a private validation method because of the second argument. */ - public function SetDomain(string $domain) : void { + public function setDomain(string $domain, bool $skipDomainValidation) : void { + // Validate that at least one dot is contained + if (!str_contains($domain, '.')) { + throw new InvalidSettingConfigurationException("Domain must contain at least one dot!"); + } + + // Validate that no slashes are contained + if (str_contains($domain, '/')) { + throw new InvalidSettingConfigurationException("Domain must not contain slashes!"); + } + + // Validate that no colons are contained + if (str_contains($domain, ':')) { + throw new InvalidSettingConfigurationException("Domain must not contain colons!"); + } + // Validate domain - if (!filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) { + if (filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) { throw new InvalidSettingConfigurationException("Domain is not a valid domain!"); } @@ -229,8 +463,9 @@ class ConfigurationManager } // Skip domain validation if opted in to do so - if (!$this->shouldDomainValidationBeSkipped()) { - + if ($this->shouldDomainValidationBeSkipped($skipDomainValidation)) { + error_log('Skipping domain validation'); + } else { $dnsRecordIP = gethostbyname($domain); if ($dnsRecordIP === $domain) { $dnsRecordIP = ''; @@ -238,7 +473,7 @@ class ConfigurationManager if (empty($dnsRecordIP)) { $record = dns_get_record($domain, DNS_AAAA); - if (!empty($record)) { + if (isset($record[0]['ipv6']) && !empty($record[0]['ipv6'])) { $dnsRecordIP = $record[0]['ipv6']; } } @@ -249,14 +484,13 @@ class ConfigurationManager } // Get the apache port - $port = $this->GetApachePort(); + $port = $this->apachePort; if (!filter_var($dnsRecordIP, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { - $errorMessage = "It seems like the ip-address is set to an internal or reserved ip-address. This is not supported. (It was found to be set to '" . $dnsRecordIP . "')"; if ($port === '443') { - throw new InvalidSettingConfigurationException($errorMessage); + throw new InvalidSettingConfigurationException("It seems like the ip-address of the domain is set to an internal or reserved ip-address. This is not supported by the domain validation. (It was found to be set to '" . $dnsRecordIP . "'). Please set it to a public ip-address so that the domain validation can work or skip the domain validation!"); } else { - error_log($errorMessage); + error_log("Info: It seems like the ip-address of " . $domain . " is set to an internal or reserved ip-address. (It was found to be set to '" . $dnsRecordIP . "')"); } } @@ -265,11 +499,11 @@ class ConfigurationManager if ($connection) { fclose($connection); } else { - throw new InvalidSettingConfigurationException("The server is not reachable on Port 443. You can verify this e.g. with 'https://portchecker.co/' by entering your domain there as ip-address and port 443 as port."); + throw new InvalidSettingConfigurationException("The domain is not reachable on Port 443 from within this container. Have you opened port 443/tcp in your router/firewall? If yes is the problem most likely that the router or firewall forbids local access to your domain. Or in other words: NAT loopback (Hairpinning) does not seem to work in your network. You can work around that by setting up a local DNS server and utilizing Split-Brain-DNS and configuring the daemon.json file of your docker daemon to use the local DNS server."); } // Get Instance ID - $instanceID = $this->GetSecret('INSTANCE_ID'); + $instanceID = $this->getAndGenerateSecret('INSTANCE_ID'); // set protocol if ($port !== '443') { @@ -280,9 +514,14 @@ class ConfigurationManager // Check if response is correct $ch = curl_init(); + if ($ch === false) { + throw new InvalidSettingConfigurationException('Could not init curl! Please check the logs!'); + } $testUrl = $protocol . $domain . ':443'; curl_setopt($ch, CURLOPT_URL, $testUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); $response = (string)curl_exec($ch); # Get rid of trailing \n $response = str_replace("\n", "", $response); @@ -291,114 +530,128 @@ class ConfigurationManager error_log('The response of the connection attempt to "' . $testUrl . '" was: ' . $response); error_log('Expected was: ' . $instanceID); error_log('The error message was: ' . curl_error($ch)); - throw new InvalidSettingConfigurationException("Domain does not point to this server or the reverse proxy is not configured correctly. See the mastercontainer logs for more details. ('sudo docker logs -f nextcloud-aio-mastercontainer')"); + $notice = "Domain does not point to this server or the reverse proxy is not configured correctly. See the mastercontainer logs for more details. ('sudo docker logs -f nextcloud-aio-mastercontainer')"; + if ($port === '443') { + $notice .= " If you should be using Cloudflare, make sure to disable the Cloudflare Proxy feature as it might block the domain validation. Same for any other firewall or service that blocks unencrypted access on port 443."; + } else { + error_log('Please follow https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#how-to-debug in order to debug things!'); + } + throw new InvalidSettingConfigurationException($notice); } } + $this->startTransaction(); // Write domain - $config = $this->GetConfig(); - $config['domain'] = $domain; + // Don't set the domain via the attribute, or we create a loop. + $this->set('domain', $domain); // Reset the borg restore password when setting the domain - $config['borg_restore_password'] = ''; - $this->WriteConfig($config); + $this->borgRestorePassword = ''; + $this->startTransaction(); + $this->commitTransaction(); } - public function GetDomain() : string { - $config = $this->GetConfig(); - if(!isset($config['domain'])) { - $config['domain'] = ''; + public function getBaseDN() : string { + $domain = $this->domain; + if ($domain === "") { + return ""; } - - return $config['domain']; - } - - public function GetBackupMode() : string { - $config = $this->GetConfig(); - if(!isset($config['backup-mode'])) { - $config['backup-mode'] = ''; - } - - return $config['backup-mode']; - } - - public function GetSelectedRestoreTime() : string { - $config = $this->GetConfig(); - if(!isset($config['selected-restore-time'])) { - $config['selected-restore-time'] = ''; - } - - return $config['selected-restore-time']; - } - - public function GetAIOURL() : string { - $config = $this->GetConfig(); - if(!isset($config['AIO_URL'])) { - $config['AIO_URL'] = ''; - } - - return $config['AIO_URL']; + return 'dc=' . implode(',dc=', explode('.', $domain)); } /** * @throws InvalidSettingConfigurationException */ - public function SetBorgBackupHostLocation(string $location) : void { - $isValidPath = false; - if (str_starts_with($location, '/') && !str_ends_with($location, '/')) { - $isValidPath = true; - } elseif ($location === 'nextcloud_aio_backupdir') { - $isValidPath = true; - } - - if (!$isValidPath) { - throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!"); - } - - - $config = $this->GetConfig(); - $config['borg_backup_host_location'] = $location; - $this->WriteConfig($config); + public function setBorgLocationVars(string $location, string $repo) : void { + $this->validateBorgLocationVars($location, $repo); + $this->startTransaction(); + $this->borgBackupHostLocation = $location; + $this->borgRemoteRepo = $repo; + $this->commitTransaction(); } - /** - * @throws InvalidSettingConfigurationException - */ - public function SetBorgRestoreHostLocationAndPassword(string $location, string $password) : void { - if ($location === '') { - throw new InvalidSettingConfigurationException("Please enter a path!"); - } - - $isValidPath = false; - if (str_starts_with($location, '/') && !str_ends_with($location, '/')) { - $isValidPath = true; - } elseif ($location === 'nextcloud_aio_backupdir') { - $isValidPath = true; + private function validateBorgLocationVars(string $location, string $repo) : void { + if ($location === '' && $repo === '') { + throw new InvalidSettingConfigurationException("Please enter a path or a remote repo url!"); + } elseif ($location !== '' && $repo !== '') { + throw new InvalidSettingConfigurationException("Location and remote repo url are mutually exclusive!"); } - if (!$isValidPath) { - throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!"); + if ($location !== '') { + $isValidPath = false; + if (str_starts_with($location, '/') && !str_ends_with($location, '/')) { + $isValidPath = true; + } elseif ($location === 'nextcloud_aio_backupdir') { + $isValidPath = true; + } + + if (!$isValidPath) { + throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'! Another option is to use the docker volume name 'nextcloud_aio_backupdir'."); + } + + // Prevent backup to be contained in Nextcloud Datadir as this will delete the backup archive upon restore + // See https://github.com/nextcloud/all-in-one/issues/6607 + if (str_starts_with($location . '/', rtrim($this->nextcloudDatadirMount, '/') . '/')) { + throw new InvalidSettingConfigurationException("The path must not be a children of or equal to NEXTCLOUD_DATADIR, which is currently set to " . $this->nextcloudDatadirMount); + } + + } else { + $this->validateBorgRemoteRepo($repo); } + } + + private function validateBorgRemoteRepo(string $repo) : void { + $commonMsg = "For valid urls, see the remote examples at https://borgbackup.readthedocs.io/en/stable/usage/general.html#repository-urls"; + if ($repo === "") { + // Ok, remote repo is optional + } elseif (!str_contains($repo, "@")) { + throw new InvalidSettingConfigurationException("The remote repo must contain '@'. $commonMsg"); + } elseif (!str_contains($repo, ":")) { + throw new InvalidSettingConfigurationException("The remote repo must contain ':'. $commonMsg"); + } + } + + public function deleteBorgBackupLocationItems() : void { + // Delete the variables + $this->startTransaction(); + $this->borgBackupHostLocation = ''; + $this->borgRemoteRepo = ''; + $this->commitTransaction(); + + // Also delete the borg config file to be able to start over + if (file_exists(DataConst::GetBackupKeyFile())) { + if (unlink(DataConst::GetBackupKeyFile())) { + error_log('borg.config file deleted to be able to start over.'); + } + } + } + + /** + * @throws InvalidSettingConfigurationException + */ + public function setBorgRestoreLocationVarsAndPassword(string $location, string $repo, string $password) : void { + $this->validateBorgLocationVars($location, $repo); if ($password === '') { throw new InvalidSettingConfigurationException("Please enter the password!"); } - $config = $this->GetConfig(); - $config['borg_backup_host_location'] = $location; - $config['borg_restore_password'] = $password; - $config['instance_restore_attempt'] = 1; - $this->WriteConfig($config); + $this->startTransaction(); + $this->borgBackupHostLocation = $location; + $this->borgRemoteRepo = $repo; + $this->borgRestorePassword = $password; + $this->instanceRestoreAttempt = true; + $this->commitTransaction(); } /** * @throws InvalidSettingConfigurationException */ - public function ChangeMasterPassword(string $currentPassword, string $newPassword) : void { + public function changeMasterPassword(string $currentPassword, string $newPassword) : void { if ($currentPassword === '') { throw new InvalidSettingConfigurationException("Please enter your current password."); } - if ($currentPassword !== $this->GetPassword()) { + if ($currentPassword !== $this->password) { throw new InvalidSettingConfigurationException("The entered current password is not correct."); } @@ -415,133 +668,69 @@ class ConfigurationManager } // All checks pass so set the password - $this->SetPassword($newPassword); - } - - public function GetApachePort() : string { - $envVariableName = 'APACHE_PORT'; - $configName = 'apache_port'; - $defaultValue = '443'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetTalkPort() : string { - $envVariableName = 'TALK_PORT'; - $configName = 'talk_port'; - $defaultValue = '3478'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + $this->set('password', $newPassword); } /** * @throws InvalidSettingConfigurationException */ - public function WriteConfig(array $config) : void { + private function writeConfig() : void { if(!is_dir(DataConst::GetDataDirectory())) { throw new InvalidSettingConfigurationException(DataConst::GetDataDirectory() . " does not exist! Something was set up falsely!"); } - file_put_contents(DataConst::GetConfigFile(), json_encode($config)); + // Shouldn't happen, but as a precaution we won't write an empty config to disk. + if ($this->config === []) { + return; + } + $df = disk_free_space(DataConst::GetDataDirectory()); + $content = json_encode($this->config, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR); + $size = strlen($content) + 10240; + if ($df !== false && (int)$df < $size) { + throw new InvalidSettingConfigurationException(DataConst::GetDataDirectory() . " does not have enough space for writing the config file! Not writing it back!"); + } + file_put_contents(DataConst::GetConfigFile(), $content); + $this->config = []; } - private function GetEnvironmentalVariableOrConfig(string $envVariableName, string $configName, string $defaultValue) : string { + private function getEnvironmentalVariableOrConfig(string $envVariableName, string $configName, string $defaultValue) : string { $envVariableOutput = getenv($envVariableName); + $configValue = $this->get($configName, ''); if ($envVariableOutput === false) { - $config = $this->GetConfig(); - if (!isset($config[$configName]) || $config[$configName] === '') { - $config[$configName] = $defaultValue; + if ($configValue === '') { + return $defaultValue; } - return $config[$configName]; + return $configValue; } - if(file_exists(DataConst::GetConfigFile())) { - $config = $this->GetConfig(); - if (!isset($config[$configName])) { - $config[$configName] = ''; - } - if ($envVariableOutput !== $config[$configName]) { - $config[$configName] = $envVariableOutput; - $this->WriteConfig($config); + + if (file_exists(DataConst::GetConfigFile())) { + if ($envVariableOutput !== $configValue) { + $this->set($configName, $envVariableOutput); } } + return $envVariableOutput; } - public function GetBorgBackupHostLocation() : string { - $config = $this->GetConfig(); - if(!isset($config['borg_backup_host_location'])) { - $config['borg_backup_host_location'] = ''; + public function getBorgPublicKey() : string { + if (!file_exists(DataConst::GetBackupPublicKey())) { + return ""; } - return $config['borg_backup_host_location']; + return trim((string)file_get_contents(DataConst::GetBackupPublicKey())); } - public function GetBorgRestorePassword() : string { - $config = $this->GetConfig(); - if(!isset($config['borg_restore_password'])) { - $config['borg_restore_password'] = ''; + public function getCollaboraSeccompPolicy() : string { + $defaultString = '--o:security.seccomp='; + if (!$this->collaboraSeccompDisabled) { + return $defaultString . 'true'; } - - return $config['borg_restore_password']; - } - - public function isInstanceRestoreAttempt() : bool { - $config = $this->GetConfig(); - if(!isset($config['instance_restore_attempt'])) { - $config['instance_restore_attempt'] = ''; - } - - if ($config['instance_restore_attempt'] === 1) { - return true; - } - return false; - } - - public function GetBorgBackupMode() : string { - $config = $this->GetConfig(); - if(!isset($config['backup-mode'])) { - $config['backup-mode'] = ''; - } - - return $config['backup-mode']; - } - - public function GetNextcloudMount() : string { - $envVariableName = 'NEXTCLOUD_MOUNT'; - $configName = 'nextcloud_mount'; - $defaultValue = ''; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetNextcloudDatadirMount() : string { - $envVariableName = 'NEXTCLOUD_DATADIR'; - $configName = 'nextcloud_datadir'; - $defaultValue = 'nextcloud_aio_nextcloud_data'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetNextcloudUploadLimit() : string { - $envVariableName = 'NEXTCLOUD_UPLOAD_LIMIT'; - $configName = 'nextcloud_upload_limit'; - $defaultValue = '10G'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetNextcloudMaxTime() : string { - $envVariableName = 'NEXTCLOUD_MAX_TIME'; - $configName = 'nextcloud_max_time'; - $defaultValue = '3600'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetDockerSocketPath() : string { - $envVariableName = 'DOCKER_SOCKET_PATH'; - $configName = 'docker_socket_path'; - $defaultValue = '/var/run/docker.sock'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + return $defaultString . 'false'; } /** * @throws InvalidSettingConfigurationException */ - public function SetDailyBackupTime(string $time, bool $enableAutomaticUpdates) : void { + public function setDailyBackupTime(string $time, bool $enableAutomaticUpdates, bool $successNotification) : void { if ($time === "") { throw new InvalidSettingConfigurationException("The daily backup time must not be empty!"); } @@ -549,18 +738,25 @@ class ConfigurationManager if (!preg_match("#^[0-1][0-9]:[0-5][0-9]$#", $time) && !preg_match("#^2[0-3]:[0-5][0-9]$#", $time)) { throw new InvalidSettingConfigurationException("You did not enter a correct time! One correct example is '04:00'!"); } - + if ($enableAutomaticUpdates === false) { $time .= PHP_EOL . 'automaticUpdatesAreNotEnabled'; + } else { + $time .= PHP_EOL; + } + if ($successNotification === false) { + $time .= PHP_EOL . 'successNotificationsAreNotEnabled'; + } else { + $time .= PHP_EOL; } file_put_contents(DataConst::GetDailyBackupTimeFile(), $time); } - public function GetDailyBackupTime() : string { + public function getDailyBackupTime() : string { if (!file_exists(DataConst::GetDailyBackupTimeFile())) { return ''; } - $dailyBackupFile = file_get_contents(DataConst::GetDailyBackupTimeFile()); + $dailyBackupFile = (string)file_get_contents(DataConst::GetDailyBackupTimeFile()); $dailyBackupFileArray = explode("\n", $dailyBackupFile); return $dailyBackupFileArray[0]; } @@ -569,7 +765,7 @@ class ConfigurationManager if (!file_exists(DataConst::GetDailyBackupTimeFile())) { return false; } - $dailyBackupFile = file_get_contents(DataConst::GetDailyBackupTimeFile()); + $dailyBackupFile = (string)file_get_contents(DataConst::GetDailyBackupTimeFile()); $dailyBackupFileArray = explode("\n", $dailyBackupFile); if (isset($dailyBackupFileArray[1]) && $dailyBackupFileArray[1] === 'automaticUpdatesAreNotEnabled') { return false; @@ -578,7 +774,7 @@ class ConfigurationManager } } - public function DeleteDailyBackupTime() : void { + public function deleteDailyBackupTime() : void { if (file_exists(DataConst::GetDailyBackupTimeFile())) { unlink(DataConst::GetDailyBackupTimeFile()); } @@ -587,14 +783,14 @@ class ConfigurationManager /** * @throws InvalidSettingConfigurationException */ - public function SetAdditionalBackupDirectories(string $additionalBackupDirectories) : void { + public function setAdditionalBackupDirectories(string $additionalBackupDirectories) : void { $additionalBackupDirectoriesArray = explode("\n", $additionalBackupDirectories); $validDirectories = ''; foreach($additionalBackupDirectoriesArray as $entry) { // Trim all unwanted chars on both sites $entry = trim($entry); if ($entry !== "") { - if (!preg_match("#^/[0-1a-zA-Z/-_]+$#", $entry) && !preg_match("#^[0-1a-zA-Z_-]+$#", $entry)) { + if (!preg_match("#^/[.0-9a-zA-Z/_-]+$#", $entry) && !preg_match("#^[.0-9a-zA-Z_-]+$#", $entry)) { throw new InvalidSettingConfigurationException("You entered unallowed characters! Problematic is " . $entry); } $validDirectories .= rtrim($entry, '/') . PHP_EOL; @@ -608,41 +804,28 @@ class ConfigurationManager } } - public function GetAdditionalBackupDirectoriesString() : string { + public function getAdditionalBackupDirectoriesString() : string { if (!file_exists(DataConst::GetAdditionalBackupDirectoriesFile())) { return ''; } - $additionalBackupDirectories = file_get_contents(DataConst::GetAdditionalBackupDirectoriesFile()); - return $additionalBackupDirectories; + return (string)file_get_contents(DataConst::GetAdditionalBackupDirectoriesFile()); } - public function GetAdditionalBackupDirectoriesArray() : array { - $additionalBackupDirectories = $this->GetAdditionalBackupDirectoriesString(); + public function getAdditionalBackupDirectoriesArray() : array { + $additionalBackupDirectories = $this->getAdditionalBackupDirectoriesString(); $additionalBackupDirectoriesArray = explode("\n", $additionalBackupDirectories); $additionalBackupDirectoriesArray = array_unique($additionalBackupDirectoriesArray, SORT_REGULAR); return $additionalBackupDirectoriesArray; } public function isDailyBackupRunning() : bool { - if (file_exists(DataConst::GetDailyBackupBlockFile())) { - return true; - } - return false; - } - - public function GetTimezone() : string { - $config = $this->GetConfig(); - if(!isset($config['timezone'])) { - $config['timezone'] = ''; - } - - return $config['timezone']; + return file_exists(DataConst::GetDailyBackupBlockFile()); } /** * @throws InvalidSettingConfigurationException */ - public function SetTimezone(string $timezone) : void { + private function validateTimezone(string $timezone) : void { if ($timezone === "") { throw new InvalidSettingConfigurationException("The timezone must not be empty!"); } @@ -650,38 +833,42 @@ class ConfigurationManager if (!preg_match("#^[a-zA-Z0-9_\-\/\+]+$#", $timezone)) { throw new InvalidSettingConfigurationException("The entered timezone does not seem to be a valid timezone!"); } - - $config = $this->GetConfig(); - $config['timezone'] = $timezone; - $this->WriteConfig($config); } - public function DeleteTimezone() : void { - $config = $this->GetConfig(); - $config['timezone'] = ''; - $this->WriteConfig($config); + /** + * Provide an extra method since our `timezone` attribute setter prevents setting an empty timezone. + */ + public function deleteTimezone() : void { + $this->set('timezone', ''); } - public function shouldDomainValidationBeSkipped() : bool { - if (getenv('SKIP_DOMAIN_VALIDATION') !== false) { + public function shouldDomainValidationBeSkipped(bool $skipDomainValidation) : bool { + if ($skipDomainValidation || getenv('SKIP_DOMAIN_VALIDATION') === 'true') { return true; } return false; } - public function GetCollaboraDictionaries() : string { - $config = $this->GetConfig(); - if(!isset($config['collabora_dictionaries'])) { - $config['collabora_dictionaries'] = ''; + public function getApacheAdditionalNetwork() : string { + $network = getenv('APACHE_ADDITIONAL_NETWORK'); + if (is_string($network)) { + return trim($network); } + return ''; + } - return $config['collabora_dictionaries']; + public function getNextcloudStartupApps() : string { + $apps = getenv('NEXTCLOUD_STARTUP_APPS'); + if (is_string($apps)) { + return trim($apps); + } + return 'deck twofactor_totp tasks calendar contacts notes'; } /** * @throws InvalidSettingConfigurationException */ - public function SetCollaboraDictionaries(string $CollaboraDictionaries) : void { + private function validateCollaboraDictionaries(string $CollaboraDictionaries) : void { if ($CollaboraDictionaries === "") { throw new InvalidSettingConfigurationException("The dictionaries must not be empty!"); } @@ -689,37 +876,190 @@ class ConfigurationManager if (!preg_match("#^[a-zA-Z_ ]+$#", $CollaboraDictionaries)) { throw new InvalidSettingConfigurationException("The entered dictionaries do not seem to be a valid!"); } - - $config = $this->GetConfig(); - $config['collabora_dictionaries'] = $CollaboraDictionaries; - $this->WriteConfig($config); } - public function DeleteCollaboraDictionaries() : void { - $config = $this->GetConfig(); - $config['collabora_dictionaries'] = ''; - $this->WriteConfig($config); + /** + * Provide an extra method since the corresponding attribute setter prevents setting an empty value. + */ + public function deleteCollaboraDictionaries() : void { + $this->set('collabora_dictionaries', ''); } - public function GetApacheIPBinding() : string { - $envVariableName = 'APACHE_IP_BINDING'; - $configName = 'apache_ip_binding'; - $defaultValue = ''; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } + /** + * @throws InvalidSettingConfigurationException + */ + private function validateCollaboraAdditionalOptions(string $additionalCollaboraOptions) : void { + if ($additionalCollaboraOptions === "") { + throw new InvalidSettingConfigurationException("The additional options must not be empty!"); + } - private function GetDisableBackupSection() : string { - $envVariableName = 'DISABLE_BACKUP_SECTION'; - $configName = 'disable_backup_section'; - $defaultValue = ''; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function isBackupSectionEnabled() : bool { - if ($this->GetDisableBackupSection() === 'true') { - return false; - } else { - return true; + if (!preg_match("#^--o:#", $additionalCollaboraOptions)) { + throw new InvalidSettingConfigurationException("The entered options must start with '--o:'. So the config does not seem to be a valid!"); } } + + public function isCollaboraSubscriptionEnabled() : bool { + return str_contains($this->collaboraAdditionalOptions, '--o:support_key='); + } + + /** + * Provide an extra method since the corresponding attribute setter prevents setting an empty value. + */ + public function deleteAdditionalCollaboraOptions() : void { + $this->set('collabora_additional_options', ''); + } + + public function listAvailableCommunityContainers() : array { + $cc = []; + $dir = scandir(DataConst::GetCommunityContainersDirectory()); + if ($dir === false) { + return $cc; + } + // Get rid of dots from the scandir command + $dir = array_diff($dir, array('..', '.', 'readme.md')); + foreach ($dir as $id) { + $filePath = DataConst::GetCommunityContainersDirectory() . '/' . $id . '/' . $id . '.json'; + $fileContents = apcu_fetch($filePath); + if (!is_string($fileContents)) { + $fileContents = file_get_contents($filePath); + if (is_string($fileContents)) { + apcu_add($filePath, $fileContents); + } + } + $json = is_string($fileContents) ? json_decode($fileContents, true, 512, JSON_THROW_ON_ERROR) : false; + if(is_array($json) && is_array($json['aio_services_v1'])) { + foreach ($json['aio_services_v1'] as $service) { + $documentation = is_string($service['documentation']) ? $service['documentation'] : ''; + if (is_string($service['display_name'])) { + $cc[$id] = [ + 'id' => $id, + 'name' => $service['display_name'], + 'documentation' => $documentation + ]; + } + break; + } + } + } + return $cc; + } + + private function camelize(string $input, string $delimiter = '_') : string { + if ($input === '') { + throw new InvalidSettingConfigurationException('input cannot be empty!'); + } + if ($delimiter === '') { + $delimiter = '_'; + } + return lcfirst(implode("", array_map('ucfirst', explode($delimiter, strtolower($input))))); + + } + + public function setAioVariables(array $input) : void { + if ($input === []) { + return; + } + $this->startTransaction(); + foreach ($input as $variable) { + if (!is_string($variable) || !str_contains($variable, '=')) { + error_log("Invalid input: '$variable' is not a string or does not contain an equal sign ('=')"); + continue; + } + $keyWithValue = $this->replaceEnvPlaceholders($variable); + // Pad the result with nulls so psalm is happy (and we don't risk to run into warnings in case + // the check for an equal sign from above gets changed). + [$key, $value] = explode('=', $keyWithValue, 2) + [null, null]; + $key = $this->camelize($key); + if ($value === null) { + error_log("Invalid input: '$keyWithValue' has no value after the equal sign"); + } else if (!property_exists($this, $key)) { + error_log("Error: '$key' is not a valid configuration key (in '$keyWithValue')"); + } else { + $this->$key = $value; + } + } + $this->commitTransaction(); + } + + // + // Replaces placeholders in $envValue with their values. + // E.g. "%NC_DOMAIN%:%APACHE_PORT" becomes "my.nextcloud.com:11000" + public function replaceEnvPlaceholders(string $envValue): string { + // $pattern breaks down as: + // % - matches a literal percent sign + // ([^%]+) - capture group that matches one or more characters that are NOT percent signs + // % - matches the closing percent sign + // + // Assumes literal percent signs are always matched and there is no + // escaping. + $pattern = '/%([^%]+)%/'; + $matchCount = preg_match_all($pattern, $envValue, $matches); + + if ($matchCount === 0) { + return $envValue; + } + + $placeholders = $matches[0]; // ["%PLACEHOLDER1%", "%PLACEHOLDER2%", ...] + $placeholderNames = $matches[1]; // ["PLACEHOLDER1", "PLACEHOLDER2", ...] + $placeholderPatterns = array_map(static fn(string $p) => '/' . preg_quote($p) . '/', $placeholders); // ["/%PLACEHOLDER1%/", ...] + $placeholderValues = array_map($this->getPlaceholderValue(...), $placeholderNames); // ["val1", "val2"] + // Guaranteed to be non-null because we found the placeholders in the preg_match_all. + return (string) preg_replace($placeholderPatterns, $placeholderValues, $envValue); + } + + private function getPlaceholderValue(string $placeholder) : string { + return match ($placeholder) { + 'NC_DOMAIN' => $this->domain, + 'NC_BASE_DN' => $this->getBaseDN(), + 'AIO_TOKEN' => $this->aioToken, + 'BORGBACKUP_REMOTE_REPO' => $this->borgRemoteRepo, + 'BORGBACKUP_MODE' => $this->backupMode, + 'AIO_URL' => $this->aioUrl, + 'SELECTED_RESTORE_TIME' => $this->selectedRestoreTime, + 'RESTORE_EXCLUDE_PREVIEWS' => $this->restoreExcludePreviews ? '1' : '', + 'APACHE_PORT' => $this->apachePort, + 'APACHE_IP_BINDING' => $this->apacheIpBinding, + 'TALK_PORT' => $this->talkPort, + 'TURN_DOMAIN' => $this->turnDomain, + 'NEXTCLOUD_MOUNT' => $this->nextcloudMount, + 'BACKUP_RESTORE_PASSWORD' => $this->borgRestorePassword, + 'CLAMAV_ENABLED' => $this->isClamavEnabled ? 'yes' : '', + 'TALK_RECORDING_ENABLED' => $this->isTalkRecordingEnabled ? 'yes' : '', + 'ONLYOFFICE_ENABLED' => $this->isOnlyofficeEnabled ? 'yes' : '', + 'COLLABORA_ENABLED' => $this->isCollaboraEnabled ? 'yes' : '', + 'TALK_ENABLED' => $this->isTalkEnabled ? 'yes' : '', + 'UPDATE_NEXTCLOUD_APPS' => ($this->isDailyBackupRunning() && $this->areAutomaticUpdatesEnabled()) ? 'yes' : '', + 'TIMEZONE' => $this->timezone === '' ? 'Etc/UTC' : $this->timezone, + 'COLLABORA_DICTIONARIES' => $this->collaboraDictionaries === '' ? 'de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru' : $this->collaboraDictionaries, + 'IMAGINARY_ENABLED' => $this->isImaginaryEnabled ? 'yes' : '', + 'FULLTEXTSEARCH_ENABLED' => $this->isFulltextsearchEnabled ? 'yes' : '', + 'DOCKER_SOCKET_PROXY_ENABLED' => $this->isDockerSocketProxyEnabled ? 'yes' : '', + 'NEXTCLOUD_UPLOAD_LIMIT' => $this->nextcloudUploadLimit, + 'NEXTCLOUD_MEMORY_LIMIT' => $this->nextcloudMemoryLimit, + 'NEXTCLOUD_MAX_TIME' => $this->nextcloudMaxTime, + 'BORG_RETENTION_POLICY' => $this->borgRetentionPolicy, + 'FULLTEXTSEARCH_JAVA_OPTIONS' => $this->fulltextsearchJavaOptions, + 'NEXTCLOUD_TRUSTED_CACERTS_DIR' => $this->trustedCacertsDir, + 'ADDITIONAL_DIRECTORIES_BACKUP' => $this->getAdditionalBackupDirectoriesString() !== '' ? 'yes' : '', + 'BORGBACKUP_HOST_LOCATION' => $this->borgBackupHostLocation, + 'APACHE_MAX_SIZE' => (string)($this->getApacheMaxSize()), + 'COLLABORA_SECCOMP_POLICY' => $this->getCollaboraSeccompPolicy(), + 'NEXTCLOUD_STARTUP_APPS' => $this->getNextcloudStartupApps(), + 'NEXTCLOUD_ADDITIONAL_APKS' => $this->nextcloudAdditionalApks, + 'NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS' => $this->nextcloudAdditionalPhpExtensions, + 'INSTALL_LATEST_MAJOR' => $this->installLatestMajor ? 'yes' : '', + 'REMOVE_DISABLED_APPS' => $this->nextcloudKeepDisabledApps ? '' : 'yes', + // Allow to get local ip-address of database container which allows to talk to it even in host mode (the container that requires this needs to be started first then) + 'AIO_DATABASE_HOST' => gethostbyname('nextcloud-aio-database'), + // Allow to get local ip-address of caddy container and add it to trusted proxies automatically + 'CADDY_IP_ADDRESS' => in_array('caddy', $this->aioCommunityContainers, true) ? gethostbyname('nextcloud-aio-caddy') : '', + 'WHITEBOARD_ENABLED' => $this->isWhiteboardEnabled ? 'yes' : '', + 'AIO_VERSION' => $this->getAioVersion(), + default => $this->getRegisteredSecret($placeholder), + }; + } + + private function booleanize(mixed $value) : bool { + return in_array($value, [true, 'true'], true); + } } diff --git a/php/src/Data/DataConst.php b/php/src/Data/DataConst.php index 5e671c11..9272e3d4 100644 --- a/php/src/Data/DataConst.php +++ b/php/src/Data/DataConst.php @@ -8,7 +8,7 @@ class DataConst { return '/mnt/docker-aio-config/data/'; } - return realpath(__DIR__ . '/../../data/'); + return (string)realpath(__DIR__ . '/../../data/'); } public static function GetSessionDirectory() : string { @@ -16,13 +16,17 @@ class DataConst { return '/mnt/docker-aio-config/session/'; } - return realpath(__DIR__ . '/../../session/'); + return (string)realpath(__DIR__ . '/../../session/'); } public static function GetConfigFile() : string { return self::GetDataDirectory() . '/configuration.json'; } + public static function GetBackupPublicKey() : string { + return self::GetDataDirectory() . '/id_borg.pub'; + } + public static function GetBackupSecretFile() : string { return self::GetDataDirectory() . '/backupsecret'; } @@ -46,4 +50,24 @@ class DataConst { public static function GetBackupArchivesList() : string { return self::GetDataDirectory() . '/backup_archives.list'; } + + public static function GetSessionDateFile() : string { + return self::GetDataDirectory() . '/session_date_file'; + } + + public static function GetCommunityContainersDirectory() : string { + return (string)realpath(__DIR__ . '/../../../community-containers/'); + } + + public static function GetCollaboraSeccompProfilePath() : string { + return (string)realpath(__DIR__ . '/../../cool-seccomp-profile.json'); + } + + public static function GetContainersDefinitionPath() : string { + return (string)realpath(__DIR__ . '/../../containers.json'); + } + + public static function GetAioVersionFile() : string { + return (string)realpath(__DIR__ . '/../../templates/includes/aio-version.twig'); + } } diff --git a/php/src/Data/Setup.php b/php/src/Data/Setup.php index 2ab87e36..e409eef8 100644 --- a/php/src/Data/Setup.php +++ b/php/src/Data/Setup.php @@ -4,16 +4,11 @@ namespace AIO\Data; use AIO\Auth\PasswordGenerator; -class Setup -{ - private PasswordGenerator $passwordGenerator; - private ConfigurationManager $configurationManager; - +readonly class Setup { public function __construct( - PasswordGenerator $passwordGenerator, - ConfigurationManager $configurationManager) { - $this->passwordGenerator = $passwordGenerator; - $this->configurationManager = $configurationManager; + private PasswordGenerator $passwordGenerator, + private ConfigurationManager $configurationManager, + ) { } public function Setup() : string { @@ -22,7 +17,7 @@ class Setup } $password = $this->passwordGenerator->GeneratePassword(8); - $this->configurationManager->SetPassword($password); + $this->configurationManager->password = $password; return $password; } diff --git a/php/src/DependencyInjection.php b/php/src/DependencyInjection.php index e37a0917..1fedada8 100644 --- a/php/src/DependencyInjection.php +++ b/php/src/DependencyInjection.php @@ -4,6 +4,7 @@ namespace AIO; use AIO\Docker\DockerHubManager; use DI\Container; +use AIO\Docker\GitHubContainerRegistryManager; class DependencyInjection { @@ -15,6 +16,11 @@ class DependencyInjection new DockerHubManager() ); + $container->set( + GitHubContainerRegistryManager::class, + new GitHubContainerRegistryManager() + ); + $container->set( \AIO\Data\ConfigurationManager::class, new \AIO\Data\ConfigurationManager() @@ -24,7 +30,8 @@ class DependencyInjection new \AIO\Docker\DockerActionManager( $container->get(\AIO\Data\ConfigurationManager::class), $container->get(\AIO\ContainerDefinitionFetcher::class), - $container->get(DockerHubManager::class) + $container->get(DockerHubManager::class), + $container->get(GitHubContainerRegistryManager::class) ) ); $container->set( diff --git a/php/src/Docker/DockerActionManager.php b/php/src/Docker/DockerActionManager.php index 43f84deb..86b36619 100644 --- a/php/src/Docker/DockerActionManager.php +++ b/php/src/Docker/DockerActionManager.php @@ -3,144 +3,138 @@ namespace AIO\Docker; use AIO\Container\Container; -use AIO\Container\State\IContainerState; -use AIO\Container\State\ImageDoesNotExistState; -use AIO\Container\State\StartingState; -use AIO\Container\State\RunningState; -use AIO\Container\State\RestartingState; -use AIO\Container\State\NotRestartingState; -use AIO\Container\State\VersionDifferentState; -use AIO\Container\State\StoppedState; -use AIO\Container\State\VersionEqualState; -use AIO\Data\ConfigurationManager; -use GuzzleHttp\Exception\RequestException; +use AIO\Container\ContainerState; +use AIO\Container\VersionState; use AIO\ContainerDefinitionFetcher; +use AIO\Data\ConfigurationManager; +use AIO\Data\DataConst; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\RequestException; use http\Env\Response; -class DockerActionManager -{ - private const API_VERSION = 'v1.41'; - private \GuzzleHttp\Client $guzzleClient; - private ConfigurationManager $configurationManager; - private ContainerDefinitionFetcher $containerDefinitionFetcher; - private DockerHubManager $dockerHubManager; +readonly class DockerActionManager { + private const string API_VERSION = 'v1.44'; + private Client $guzzleClient; public function __construct( - ConfigurationManager $configurationManager, - ContainerDefinitionFetcher $containerDefinitionFetcher, - DockerHubManager $dockerHubManager + private ConfigurationManager $configurationManager, + private ContainerDefinitionFetcher $containerDefinitionFetcher, + private DockerHubManager $dockerHubManager, + private GitHubContainerRegistryManager $gitHubContainerRegistryManager ) { - $this->configurationManager = $configurationManager; - $this->containerDefinitionFetcher = $containerDefinitionFetcher; - $this->dockerHubManager = $dockerHubManager; - $this->guzzleClient = new \GuzzleHttp\Client( - [ - 'curl' => [ - CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock', - - ], - ] - ); + $this->guzzleClient = new Client(['curl' => [CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock']]); } - private function BuildApiUrl(string $url) : string { - return sprintf('http://localhost/%s/%s', self::API_VERSION, $url); + private function BuildApiUrl(string $url): string { + $apiVersion = getenv('DOCKER_API_VERSION'); + if ($apiVersion === false || empty($apiVersion)) { + $apiVersion = self::API_VERSION; + } else { + $apiVersion = 'v'. $apiVersion; + } + return sprintf('http://127.0.0.1/%s/%s', $apiVersion, $url); } - private function BuildImageName(Container $container) : string { - return $container->GetContainerName() . ':' . $this->GetCurrentChannel(); + private function BuildImageName(Container $container): string { + $tag = $container->imageTag; + if ($tag === '%AIO_CHANNEL%') { + $tag = $this->GetCurrentChannel(); + } + return $container->containerName . ':' . $tag; } - public function GetContainerRunningState(Container $container) : IContainerState - { - $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); + public function GetContainerRunningState(Container $container): ContainerState { + $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->identifier))); try { $response = $this->guzzleClient->get($url); } catch (RequestException $e) { if ($e->getCode() === 404) { - return new ImageDoesNotExistState(); + return ContainerState::ImageDoesNotExist; } throw $e; } - $responseBody = json_decode((string)$response->getBody(), true); + $responseBody = json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR); if ($responseBody['State']['Running'] === true) { - return new RunningState(); + return ContainerState::Running; } else { - return new StoppedState(); + return ContainerState::Stopped; } } - public function GetContainerRestartingState(Container $container) : IContainerState - { - $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); + public function GetContainerRestartingState(Container $container): ContainerState { + $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->identifier))); try { $response = $this->guzzleClient->get($url); } catch (RequestException $e) { if ($e->getCode() === 404) { - return new ImageDoesNotExistState(); + return ContainerState::ImageDoesNotExist; } throw $e; } - $responseBody = json_decode((string)$response->getBody(), true); + $responseBody = json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR); if ($responseBody['State']['Restarting'] === true) { - return new RestartingState(); + return ContainerState::Restarting; } else { - return new NotRestartingState(); + return ContainerState::NotRestarting; } } - public function GetContainerUpdateState(Container $container) : IContainerState - { - $tag = $this->GetCurrentChannel(); + public function GetContainerUpdateState(Container $container): VersionState { + $tag = $container->imageTag; + if ($tag === '%AIO_CHANNEL%') { + $tag = $this->GetCurrentChannel(); + } - $runningDigests = $this->GetRepoDigestsOfContainer($container->GetIdentifier()); + $runningDigests = $this->GetRepoDigestsOfContainer($container->identifier); if ($runningDigests === null) { - return new VersionDifferentState(); + return VersionState::Different; } - $remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag); + $remoteDigest = $this->GetLatestDigestOfTag($container->containerName, $tag); if ($remoteDigest === null) { - return new VersionEqualstate(); + return VersionState::Equal; } - foreach($runningDigests as $runningDigest) { + foreach ($runningDigests as $runningDigest) { if ($runningDigest === $remoteDigest) { - return new VersionEqualState(); + return VersionState::Equal; } } - return new VersionDifferentState(); + return VersionState::Different; } - public function GetContainerStartingState(Container $container) : IContainerState - { + public function GetContainerStartingState(Container $container): ContainerState { $runningState = $this->GetContainerRunningState($container); - if ($runningState instanceof StoppedState) { - return new StoppedState(); - } elseif ($runningState instanceof ImageDoesNotExistState) { - return new ImageDoesNotExistState(); + if ($runningState === ContainerState::Stopped || $runningState === ContainerState::ImageDoesNotExist) { + return $runningState; } - $containerName = $container->GetIdentifier(); - if ($container->GetInternalPorts() !== null) { - foreach($container->GetInternalPorts()->GetInternalPorts() as $internalPort) { - $connection = @fsockopen($containerName, $internalPort, $errno, $errstr, 0.1); - if ($connection) { - fclose($connection); - return new RunningState(); - } else { - return new StartingState(); - } + $containerName = $container->identifier; + $internalPort = $container->internalPorts; + if ($internalPort === '%APACHE_PORT%') { + $internalPort = $this->configurationManager->apachePort; + } elseif ($internalPort === '%TALK_PORT%') { + $internalPort = $this->configurationManager->talkPort; + } + + if ($internalPort !== "" && $internalPort !== 'host') { + $connection = @fsockopen($containerName, (int)$internalPort, $errno, $errstr, 0.2); + if ($connection) { + fclose($connection); + return ContainerState::Running; + } else { + return ContainerState::Starting; } } else { - return new RunningState(); + return ContainerState::Running; } } - public function DeleteContainer(Container $container) : void { - $url = $this->BuildApiUrl(sprintf('containers/%s?v=true', urlencode($container->GetIdentifier()))); + public function DeleteContainer(Container $container): void { + $url = $this->BuildApiUrl(sprintf('containers/%s?v=true', urlencode($container->identifier))); try { $this->guzzleClient->delete($url); } catch (RequestException $e) { @@ -150,11 +144,10 @@ class DockerActionManager } } - public function GetLogs(string $id) : string - { + public function GetLogs(string $id): string { $url = $this->BuildApiUrl( sprintf( - 'containers/%s/logs?stdout=true&stderr=true', + 'containers/%s/logs?stdout=true&stderr=true×tamps=true', urlencode($id) )); $responseBody = (string)$this->guzzleClient->get($url)->getBody(); @@ -162,25 +155,28 @@ class DockerActionManager $response = ""; $separator = "\r\n"; $line = strtok($responseBody, $separator); - $response = substr($line, 8) . "\n"; + $response = substr((string)$line, 8) . $separator; while ($line !== false) { $line = strtok($separator); - $response .= substr($line, 8) . "\n"; + $response .= substr((string)$line, 8) . $separator; } return $response; } - public function StartContainer(Container $container) : void { - $url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->GetIdentifier()))); - $this->guzzleClient->post($url); + public function StartContainer(Container $container): void { + $url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->identifier))); + try { + $this->guzzleClient->post($url); + } catch (RequestException $e) { + throw new \Exception("Could not start container " . $container->identifier . ": " . $e->getResponse()?->getBody()->getContents()); + } } - public function CreateVolumes(Container $container): void - { + public function CreateVolumes(Container $container): void { $url = $this->BuildApiUrl('volumes/create'); - foreach($container->GetVolumes()->GetVolumes() as $volume) { + foreach ($container->volumes->GetVolumes() as $volume) { $forbiddenChars = [ '/', ]; @@ -190,7 +186,7 @@ class DockerActionManager } $firstChar = substr($volume->name, 0, 1); - if(!in_array($firstChar, $forbiddenChars)) { + if (!in_array($firstChar, $forbiddenChars)) { $this->guzzleClient->request( 'POST', $url, @@ -204,11 +200,18 @@ class DockerActionManager } } - public function CreateContainer(Container $container) : void { + public function CreateContainer(Container $container): void { $volumes = []; - foreach($container->GetVolumes()->GetVolumes() as $volume) { + foreach ($container->volumes->GetVolumes() as $volume) { + // // NEXTCLOUD_MOUNT gets added via bind-mount later on + // if ($container->identifier === 'nextcloud-aio-nextcloud') { + // if ($volume->name === $this->configurationManager->nextcloudMount) { + // continue; + // } + // } + $volumeEntry = $volume->name . ':' . $volume->mountPoint; - if($volume->isWritable) { + if ($volume->isWritable) { $volumeEntry = $volumeEntry . ':' . 'rw'; } else { $volumeEntry = $volumeEntry . ':' . 'ro'; @@ -217,169 +220,229 @@ class DockerActionManager $volumes[] = $volumeEntry; } - $exposedPorts = []; - foreach($container->GetPorts()->GetPorts() as $port) { - $exposedPorts[$port] = null; - } - $requestBody = [ 'Image' => $this->BuildImageName($container), ]; - if(count($volumes) > 0) { + if (count($volumes) > 0) { $requestBody['HostConfig']['Binds'] = $volumes; } - $envs = $container->GetEnvironmentVariables()->GetVariables(); - foreach($envs as $key => $env) { - $patterns = ['/%(.*)%/']; + $this->configurationManager->setAioVariables($container->aioVariables->GetVariables()); - - if(preg_match($patterns[0], $env, $out) === 1) { - $replacements = array(); - - if($out[1] === 'NC_DOMAIN') { - $replacements[1] = $this->configurationManager->GetDomain(); - } elseif ($out[1] === 'AIO_TOKEN') { - $replacements[1] = $this->configurationManager->GetToken(); - } elseif ($out[1] === 'BORGBACKUP_MODE') { - $replacements[1] = $this->configurationManager->GetBackupMode(); - } elseif ($out[1] === 'AIO_URL') { - $replacements[1] = $this->configurationManager->GetAIOURL(); - } elseif ($out[1] === 'SELECTED_RESTORE_TIME') { - $replacements[1] = $this->configurationManager->GetSelectedRestoreTime(); - } elseif ($out[1] === 'APACHE_PORT') { - $replacements[1] = $this->configurationManager->GetApachePort(); - } elseif ($out[1] === 'TALK_PORT') { - $replacements[1] = $this->configurationManager->GetTalkPort(); - } elseif ($out[1] === 'NEXTCLOUD_MOUNT') { - $replacements[1] = $this->configurationManager->GetNextcloudMount(); - } elseif ($out[1] === 'BACKUP_RESTORE_PASSWORD') { - $replacements[1] = $this->configurationManager->GetBorgRestorePassword(); - } elseif ($out[1] === 'CLAMAV_ENABLED') { - if ($this->configurationManager->isClamavEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'ONLYOFFICE_ENABLED') { - if ($this->configurationManager->isOnlyofficeEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'COLLABORA_ENABLED') { - if ($this->configurationManager->isCollaboraEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'TALK_ENABLED') { - if ($this->configurationManager->isTalkEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'UPDATE_NEXTCLOUD_APPS') { - if ($this->configurationManager->isDailyBackupRunning() && $this->configurationManager->areAutomaticUpdatesEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'TIMEZONE') { - if ($this->configurationManager->GetTimezone() === '') { - $replacements[1] = 'UTC'; - } else { - $replacements[1] = $this->configurationManager->GetTimezone(); - } - } elseif ($out[1] === 'COLLABORA_DICTIONARIES') { - if ($this->configurationManager->GetCollaboraDictionaries() === '') { - $replacements[1] = 'de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru'; - } else { - $replacements[1] = $this->configurationManager->GetCollaboraDictionaries(); - } - } elseif ($out[1] === 'IMAGINARY_ENABLED') { - if ($this->configurationManager->isImaginaryEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'FULLTEXTSEARCH_ENABLED') { - if ($this->configurationManager->isFulltextsearchEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'NEXTCLOUD_UPLOAD_LIMIT') { - $replacements[1] = $this->configurationManager->GetNextcloudUploadLimit(); - } elseif ($out[1] === 'NEXTCLOUD_MAX_TIME') { - $replacements[1] = $this->configurationManager->GetNextcloudMaxTime(); - } elseif ($out[1] === 'ADDITIONAL_DIRECTORIES_BACKUP') { - if ($this->configurationManager->GetAdditionalBackupDirectoriesString() !== '') { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'BORGBACKUP_HOST_LOCATION') { - $replacements[1] = $this->configurationManager->GetBorgBackupHostLocation(); - } else { - $replacements[1] = $this->configurationManager->GetSecret($out[1]); - } - - $envs[$key] = preg_replace($patterns, $replacements, $env); - } + $envs = $container->containerEnvironmentVariables->GetVariables(); + // Special thing for the nextcloud container + if ($container->identifier === 'nextcloud-aio-nextcloud') { + $envs[] = $this->GetAllNextcloudExecCommands(); + } + foreach ($envs as $key => $env) { + $envs[$key] = $this->configurationManager->replaceEnvPlaceholders($env); } - if(count($envs) > 0) { + if (count($envs) > 0) { $requestBody['Env'] = $envs; } - $requestBody['HostConfig']['RestartPolicy']['Name'] = $container->GetRestartPolicy(); + $requestBody['HostConfig']['RestartPolicy']['Name'] = $container->restartPolicy; - if(count($exposedPorts) > 0) { - $requestBody['ExposedPorts'] = $exposedPorts; - foreach ($container->GetPorts()->GetPorts() as $port) { - $portNumber = explode("/", $port); - if ($this->configurationManager->GetApachePort() === $portNumber[0] && $this->configurationManager->GetApacheIPBinding() !== '') { - $requestBody['HostConfig']['PortBindings'][$port] = [ - [ - 'HostPort' => $portNumber[0], - 'HostIp' => $this->configurationManager->GetApacheIPBinding(), - ] - ]; - } else { - $requestBody['HostConfig']['PortBindings'][$port] = [ - [ - 'HostPort' => $portNumber[0], - ] - ]; + $requestBody['HostConfig']['ReadonlyRootfs'] = $container->readOnlyRootFs; + + $exposedPorts = []; + if ($container->internalPorts !== 'host') { + foreach ($container->ports->GetPorts() as $value) { + $port = $value->port; + $protocol = $value->protocol; + if ($port === '%APACHE_PORT%') { + $port = $this->configurationManager->apachePort; + // Do not expose udp if AIO is in reverse proxy mode + if ($port !== '443' && $protocol === 'udp') { + continue; + } + } else if ($port === '%TALK_PORT%') { + $port = $this->configurationManager->talkPort; } + $portWithProtocol = $port . '/' . $protocol; + $exposedPorts[$portWithProtocol] = null; + } + $requestBody['HostConfig']['NetworkMode'] = 'nextcloud-aio'; + } else { + $requestBody['HostConfig']['NetworkMode'] = 'host'; + } + + if (count($exposedPorts) > 0) { + $requestBody['ExposedPorts'] = $exposedPorts; + foreach ($container->ports->GetPorts() as $value) { + $port = $value->port; + $protocol = $value->protocol; + if ($port === '%APACHE_PORT%') { + $port = $this->configurationManager->apachePort; + // Do not expose udp if AIO is in reverse proxy mode + if ($port !== '443' && $protocol === 'udp') { + continue; + } + } else if ($port === '%TALK_PORT%') { + $port = $this->configurationManager->talkPort; + // Skip publishing talk tcp port if it is set to 443 + if ($port === '443' && $protocol === 'tcp') { + continue; + } + } + $ipBinding = $value->ipBinding; + if ($ipBinding === '%APACHE_IP_BINDING%') { + $ipBinding = $this->configurationManager->apacheIpBinding; + // Do not expose if AIO is in internal network mode + if ($ipBinding === '@INTERNAL') { + continue; + } + } + $portWithProtocol = $port . '/' . $protocol; + $requestBody['HostConfig']['PortBindings'][$portWithProtocol] = [ + [ + 'HostPort' => $port, + 'HostIp' => $ipBinding, + ] + ]; } } + $devices = []; + foreach ($container->devices as $device) { + if ($device === '/dev/dri' && !$this->configurationManager->nextcloudEnableDriDevice) { + continue; + } + $devices[] = ["PathOnHost" => $device, "PathInContainer" => $device, "CgroupPermissions" => "rwm"]; + } + + if (count($devices) > 0) { + $requestBody['HostConfig']['Devices'] = $devices; + } + + if ($container->enableNvidiaGpu && $this->configurationManager->enableNvidiaGpu) { + $requestBody['HostConfig']['Runtime'] = 'nvidia'; + $requestBody['HostConfig']['DeviceRequests'] = [ + [ + "Driver" => "nvidia", + "Count" => 1, + "Capabilities" => [["gpu"]], + ] + ]; + } + + $shmSize = $container->shmSize; + if ($shmSize > 0) { + $requestBody['HostConfig']['ShmSize'] = $shmSize; + } + + $tmpfs = []; + foreach ($container->tmpfs as $tmp) { + $mode = ""; + if (str_contains($tmp, ':')) { + $mode = explode(':', $tmp)[1]; + $tmp = explode(':', $tmp)[0]; + } + $tmpfs[$tmp] = $mode; + } + if (count($tmpfs) > 0) { + $requestBody['HostConfig']['Tmpfs'] = $tmpfs; + } + + $requestBody['HostConfig']['Init'] = $container->init; + + $maxShutDownTime = $container->maxShutdownTime; + if ($maxShutDownTime > 0) { + $requestBody['StopTimeout'] = $maxShutDownTime; + } + + $capAdds = $container->capAdd; + if (count($capAdds) > 0) { + $requestBody['HostConfig']['CapAdd'] = $capAdds; + } + + // Disable arp spoofing + if (!in_array('NET_RAW', $capAdds, true)) { + $requestBody['HostConfig']['CapDrop'] = ['NET_RAW']; + } + + // Disable SELinux for AIO containers so that it does not break them + $requestBody['HostConfig']['SecurityOpt'] = ["label:disable"]; + if ($container->apparmorUnconfined) { + $requestBody['HostConfig']['SecurityOpt'] = ["apparmor:unconfined", "label:disable"]; + } + + $mounts = []; + // Special things for the backup container which should not be exposed in the containers.json - if ($container->GetIdentifier() === 'nextcloud-aio-borgbackup') { - $requestBody['HostConfig']['CapAdd'] = ["SYS_ADMIN"]; - $requestBody['HostConfig']['Devices'] = [["PathOnHost" => "/dev/fuse", "PathInContainer" => "/dev/fuse", "CgroupPermissions" => "rwm"]]; - $requestBody['HostConfig']['SecurityOpt'] = ["apparmor:unconfined"]; - + if (str_starts_with($container->identifier, 'nextcloud-aio-borgbackup')) { // Additional backup directories - $mounts = []; - foreach ($this->configurationManager->GetAdditionalBackupDirectoriesArray() as $additionalBackupDirectories) { + foreach ($this->getAllBackupVolumes() as $additionalBackupVolumes) { + if ($additionalBackupVolumes !== '') { + $mounts[] = ["Type" => "volume", "Source" => $additionalBackupVolumes, "Target" => "/nextcloud_aio_volumes/" . $additionalBackupVolumes, "ReadOnly" => false]; + } + } + + // Make volumes read only in case of borgbackup container. The viewer makes them writeable + $isReadOnly = $container->identifier === 'nextcloud-aio-borgbackup'; + + foreach ($this->configurationManager->getAdditionalBackupDirectoriesArray() as $additionalBackupDirectories) { if ($additionalBackupDirectories !== '') { if (!str_starts_with($additionalBackupDirectories, '/')) { - $mounts[] = ["Type" => "volume", "Source" => $additionalBackupDirectories, "Target" => "/docker_volumes/" . $additionalBackupDirectories, "ReadOnly" => true]; + $mounts[] = ["Type" => "volume", "Source" => $additionalBackupDirectories, "Target" => "/docker_volumes/" . $additionalBackupDirectories, "ReadOnly" => $isReadOnly]; } else { - $mounts[] = ["Type" => "bind", "Source" => $additionalBackupDirectories, "Target" => "/host_mounts" . $additionalBackupDirectories, "ReadOnly" => true, "BindOptions" => ["NonRecursive" => true]]; + $mounts[] = ["Type" => "bind", "Source" => $additionalBackupDirectories, "Target" => "/host_mounts" . $additionalBackupDirectories, "ReadOnly" => $isReadOnly, "BindOptions" => ["NonRecursive" => true]]; } } } - if(count($mounts) > 0) { - $requestBody['HostConfig']['Mounts'] = $mounts; + + // Special things for the talk container which should not be exposed in the containers.json + } elseif ($container->identifier === 'nextcloud-aio-talk') { + // This is needed due to a bug in libwebsockets used in Janus which cannot handle unlimited ulimits + $requestBody['HostConfig']['Ulimits'] = [["Name" => "nofile", "Hard" => 200000, "Soft" => 200000]]; + // // Special things for the nextcloud container which should not be exposed in the containers.json + // } elseif ($container->identifier === 'nextcloud-aio-nextcloud') { + // foreach ($container->volumes->GetVolumes() as $volume) { + // if ($volume->name !== $this->configurationManager->nextcloudMount) { + // continue; + // } + // $mounts[] = ["Type" => "bind", "Source" => $volume->name, "Target" => $volume->mountPoint, "ReadOnly" => !$volume->isWritable, "BindOptions" => [ "Propagation" => "rshared"]]; + // } + + // Special things for the caddy community container + } elseif ($container->identifier === 'nextcloud-aio-caddy') { + $requestBody['HostConfig']['ExtraHosts'] = ['host.docker.internal:host-gateway']; + + // Special things for the collabora container which should not be exposed in the containers.json + } elseif ($container->identifier === 'nextcloud-aio-collabora') { + if (!$this->configurationManager->collaboraSeccompDisabled) { + // Load reference seccomp profile for collabora + $seccompProfile = (string)file_get_contents(DataConst::GetCollaboraSeccompProfilePath()); + $requestBody['HostConfig']['SecurityOpt'] = ["label:disable", "seccomp=$seccompProfile"]; + } + + // Additional Collabora options + if ($this->configurationManager->collaboraAdditionalOptions !== '') { + // Split the list of Collabora options, which are stored as a string but must be assigned as an array. + // To avoid problems with whitespace or dashes in option arguments we use a regular expression + // that splits the string at every position where a whitespace is followed by '--o:'. + // The leading whitespace is removed in the split but the following characters are not. + // Example: "--o:example_config1='some thing' --o:example_config2=something-else" -> ["--o:example_config1='some thing'", "--o:example_config2=something-else"] + $regEx = '/\s+(?=--o:)/'; + $requestBody['Cmd'] = preg_split($regEx, rtrim($this->configurationManager->collaboraAdditionalOptions)); } } - $url = $this->BuildApiUrl('containers/create?name=' . $container->GetIdentifier()); + if (count($mounts) > 0) { + $requestBody['HostConfig']['Mounts'] = $mounts; + } + + // All AIO-managed containers should not be updated externally via watchtower but gracefully by AIO's backup and update feature. + // Also DIUN should not send update notifications. See https://crazymax.dev/diun/providers/docker/#docker-labels + // Additionally set a default org.label-schema.vendor and com.docker.compose.project + $requestBody['Labels'] = ["com.centurylinklabs.watchtower.enable" => "false", "wud.watch" => "false", "diun.enable" => "false", "org.label-schema.vendor" => "Nextcloud", "com.docker.compose.project" => "nextcloud-aio"]; + + // Containers should have a fixed host name. See https://github.com/nextcloud/all-in-one/discussions/6589 + $requestBody['Hostname'] = $container->identifier; + + $url = $this->BuildApiUrl('containers/create?name=' . $container->identifier); try { $this->guzzleClient->request( 'POST', @@ -389,39 +452,100 @@ class DockerActionManager ] ); } catch (RequestException $e) { - throw $e; + throw new \Exception("Could not create container " . $container->identifier . ": " . $e->getResponse()?->getBody()->getContents()); } } - public function PullContainer(Container $container) : void - { - $url = $this->BuildApiUrl(sprintf('images/create?fromImage=%s', urlencode($this->BuildImageName($container)))); + public function isRegistryReachable(Container $container): bool { + $tag = $container->imageTag; + if ($tag === '%AIO_CHANNEL%') { + $tag = $this->GetCurrentChannel(); + } + + $remoteDigest = $this->GetLatestDigestOfTag($container->containerName, $tag); + + if ($remoteDigest === null) { + return false; + } else { + return true; + } + } + + public function PullImage(Container $container, bool $pullImage = true): void { + + // Skip database image pull if the last shutdown was not clean + if ($container->identifier === 'nextcloud-aio-database') { + if ($this->GetDatabasecontainerExitCode() > 0) { + $pullImage = false; + error_log('Not pulling the latest database image because the container was not correctly shut down.'); + } + } + + // Check if registry is reachable in order to make sure that we do not try to pull an image if it is down + // and try to mitigate issues that are arising due to that + if ($pullImage) { + if (!$this->isRegistryReachable($container)) { + $pullImage = false; + error_log('Not pulling the ' . $container->containerName . ' image for the ' . $container->identifier . ' container because the registry does not seem to be reachable.'); + } + } + + // Do not continue if $pullImage is false + if (!$pullImage) { + return; + } + + $imageName = $this->BuildImageName($container); + $encodedImageName = urlencode($imageName); + $url = $this->BuildApiUrl(sprintf('images/create?fromImage=%s', $encodedImageName)); + $imageIsThere = true; try { - $this->guzzleClient->post($url); - } catch (RequestException $e) { - error_log('Could not get image ' . $this->BuildImageName($container) . ' from docker hub. Probably due to rate limits. ' . $e->getMessage()); - // Don't exit here because it is possible that the image is already present - // and we ran into docker hub limits. - // We will exit later if not image should be available. + $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $encodedImageName)); + $this->guzzleClient->get($imageUrl)->getBody()->getContents(); + } catch (\Throwable $e) { + $imageIsThere = false; + } + + $maxRetries = 3; + for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { + try { + $this->guzzleClient->post($url); + break; + } catch (RequestException $e) { + $message = "Could not pull image " . $imageName . " (attempt $attempt/$maxRetries): " . $e->getResponse()?->getBody()->getContents(); + if ($attempt === $maxRetries) { + if ($imageIsThere === false) { + throw new \Exception($message); + } else { + error_log($message); + } + } else { + error_log($message . ' Retrying...'); + sleep(1); + } + } } } - private function isContainerUpdateAvailable(string $id) : string - { + private function isContainerUpdateAvailable(string $id): string { $container = $this->containerDefinitionFetcher->GetContainerById($id); $updateAvailable = ""; - if ($container->GetUpdateState() instanceof VersionDifferentState) { + if ($container->GetUpdateState() === VersionState::Different) { $updateAvailable = '1'; } - foreach ($container->GetDependsOn() as $dependency) { + foreach ($container->dependsOn as $dependency) { $updateAvailable .= $this->isContainerUpdateAvailable($dependency); } return $updateAvailable; } - public function isAnyUpdateAvailable() : bool { + public function isAnyUpdateAvailable(): bool { + // return early if instance is not installed + if (!$this->configurationManager->wasStartButtonClicked) { + return false; + } $id = 'nextcloud-aio-apache'; if ($this->isContainerUpdateAvailable($id) !== "") { @@ -431,19 +555,56 @@ class DockerActionManager } } - private function GetRepoDigestsOfContainer(string $containerName) : ?array { + private function getBackupVolumes(string $id): string { + $container = $this->containerDefinitionFetcher->GetContainerById($id); + + $backupVolumes = ''; + foreach ($container->backupVolumes as $backupVolume) { + $backupVolumes .= $backupVolume . ' '; + } + foreach ($container->dependsOn as $dependency) { + $backupVolumes .= $this->getBackupVolumes($dependency); + } + return $backupVolumes; + } + + private function getAllBackupVolumes(): array { + $id = 'nextcloud-aio-apache'; + $backupVolumesArray = explode(' ', $this->getBackupVolumes($id)); + return array_unique($backupVolumesArray); + } + + private function GetNextcloudExecCommands(string $id): string { + $container = $this->containerDefinitionFetcher->GetContainerById($id); + + $nextcloudExecCommands = ''; + foreach ($container->nextcloudExecCommands as $execCommand) { + $nextcloudExecCommands .= $execCommand . PHP_EOL; + } + foreach ($container->dependsOn as $dependency) { + $nextcloudExecCommands .= $this->GetNextcloudExecCommands($dependency); + } + return $nextcloudExecCommands; + } + + private function GetAllNextcloudExecCommands(): string { + $id = 'nextcloud-aio-apache'; + return 'NEXTCLOUD_EXEC_COMMANDS=' . $this->GetNextcloudExecCommands($id); + } + + private function GetRepoDigestsOfContainer(string $containerName): ?array { try { $containerUrl = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); - $containerOutput = json_decode($this->guzzleClient->get($containerUrl)->getBody()->getContents(), true); + $containerOutput = json_decode($this->guzzleClient->get($containerUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); $imageName = $containerOutput['Image']; $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName)); - $imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true); + $imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); if (!isset($imageOutput['RepoDigests'])) { error_log('RepoDigests is not set of container ' . $containerName); return null; - } + } if (!is_array($imageOutput['RepoDigests'])) { error_log('RepoDigests of ' . $containerName . ' is not an array which is not allowed!'); @@ -452,7 +613,7 @@ class DockerActionManager $repoDigestArray = []; $oneDigestGiven = false; - foreach($imageOutput['RepoDigests'] as $repoDigest) { + foreach ($imageOutput['RepoDigests'] as $repoDigest) { $digestPosition = strpos($repoDigest, '@'); if ($digestPosition === false) { error_log('Somehow the RepoDigest of ' . $containerName . ' does not contain a @.'); @@ -472,28 +633,52 @@ class DockerActionManager } } - public function GetCurrentChannel() : string { + private function GetCurrentImageName(): string { + $cacheKey = 'aio-image-name'; + $imageName = apcu_fetch($cacheKey); + if ($imageName !== false && is_string($imageName)) { + return $imageName; + } + + $containerName = 'nextcloud-aio-mastercontainer'; + $url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); + try { + $output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); + $imageNameArray = explode(':', $output['Config']['Image']); + if (count($imageNameArray) === 2) { + $imageName = $imageNameArray[0]; + } else { + error_log("No tag was found when getting the current channel. You probably did not follow the documentation correctly. Changing the imageName to the default " . $output['Config']['Image']); + $imageName = $output['Config']['Image']; + } + apcu_add($cacheKey, $imageName); + return $imageName; + } catch (\Exception $e) { + error_log('Could not get current imageName ' . $e->getMessage()); + } + + return 'nextcloud/all-in-one'; + } + + public function GetCurrentChannel(): string { $cacheKey = 'aio-ChannelName'; $channelName = apcu_fetch($cacheKey); - if($channelName !== false && is_string($channelName)) { + if ($channelName !== false && is_string($channelName)) { return $channelName; } $containerName = 'nextcloud-aio-mastercontainer'; $url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); try { - $output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true); - $containerChecksum = $output['Image']; + $output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); $tagArray = explode(':', $output['Config']['Image']); - $tag = $tagArray[1]; - apcu_add($cacheKey, $tag); - /** - * @psalm-suppress TypeDoesNotContainNull - */ - if ($tag === null) { + if (count($tagArray) === 2) { + $tag = $tagArray[1]; + } else { error_log("No tag was found when getting the current channel. You probably did not follow the documentation correctly. Changing the channel to the default 'latest'."); $tag = 'latest'; } + apcu_add($cacheKey, $tag); return $tag; } catch (\Exception $e) { error_log('Could not get current channel ' . $e->getMessage()); @@ -502,9 +687,8 @@ class DockerActionManager return 'latest'; } - public function IsMastercontainerUpdateAvailable() : bool - { - $imageName = 'nextcloud/all-in-one'; + public function IsMastercontainerUpdateAvailable(): bool { + $imageName = $this->GetCurrentImageName(); $containerName = 'nextcloud-aio-mastercontainer'; $tag = $this->GetCurrentChannel(); @@ -513,7 +697,7 @@ class DockerActionManager if ($runningDigests === null) { return true; } - $remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($imageName, $tag); + $remoteDigest = $this->GetLatestDigestOfTag($imageName, $tag); if ($remoteDigest === null) { return false; } @@ -526,11 +710,10 @@ class DockerActionManager return true; } - public function sendNotification(Container $container, string $subject, string $message) : void - { - if ($this->GetContainerStartingState($container) instanceof RunningState) { + public function sendNotification(Container $container, string $subject, string $message, string $file = '/notify.sh'): void { + if ($this->GetContainerStartingState($container) === ContainerState::Running) { - $containerName = $container->GetIdentifier(); + $containerName = $container->identifier; // schedule the exec $url = $this->BuildApiUrl(sprintf('containers/%s/exec', urlencode($containerName))); @@ -544,17 +727,18 @@ class DockerActionManager 'Tty' => true, 'Cmd' => [ 'bash', - '/notify.sh', + $file, $subject, $message ], ], ] )->getBody()->getContents(), - true + true, + 512, + JSON_THROW_ON_ERROR, ); - // get the id from the response $id = $response['Id']; // start the exec @@ -572,57 +756,12 @@ class DockerActionManager } } - public function DisconnectContainerFromNetwork(Container $container) : void - { + private function DisconnectContainerFromBridgeNetwork(string $id): void { $url = $this->BuildApiUrl( - sprintf('networks/%s/disconnect', 'nextcloud-aio') + sprintf('networks/%s/disconnect', 'bridge') ); - try { - $this->guzzleClient->request( - 'POST', - $url, - [ - 'json' => [ - 'container' => $container->GetIdentifier(), - ], - ] - ); - } catch (RequestException $e) { - error_log('Could not disconnect container from network ' . $e->getMessage()); - } - } - - private function ConnectContainerIdToNetwork(string $id) : void - { - $url = $this->BuildApiUrl('networks/create'); - try { - $this->guzzleClient->request( - 'POST', - $url, - [ - 'json' => [ - 'Name' => 'nextcloud-aio', - 'CheckDuplicate' => true, - 'Driver' => 'bridge', - 'Internal' => false, - 'Options' => [ - 'com.docker.network.bridge.enable_icc' => 'true' - ] - ] - ] - ); - } catch (RequestException $e) { - // 409 is undocumented and gets thrown if the network already exists. - if ($e->getCode() !== 409) { - throw $e; - } - } - - $url = $this->BuildApiUrl( - sprintf('networks/%s/connect', 'nextcloud-aio') - ); try { $this->guzzleClient->request( 'POST', @@ -630,7 +769,55 @@ class DockerActionManager [ 'json' => [ 'container' => $id, + ], + ] + ); + } catch (RequestException $e) { + } + } + + private function ConnectContainerIdToNetwork(string $id, string $internalPort, string $network = 'nextcloud-aio', bool $createNetwork = true, string $alias = ''): void { + if ($internalPort === 'host') { + return; + } + + if ($createNetwork) { + $url = $this->BuildApiUrl('networks/create'); + try { + $this->guzzleClient->request( + 'POST', + $url, + [ + 'json' => [ + 'Name' => $network, + 'CheckDuplicate' => true, + 'Driver' => 'bridge', + 'Internal' => false, + ] ] + ); + } catch (RequestException $e) { + // 409 is undocumented and gets thrown if the network already exists. + if ($e->getCode() !== 409) { + throw new \Exception("Could not create the nextcloud-aio network: " . $e->getResponse()?->getBody()->getContents()); + } + } + } + + $url = $this->BuildApiUrl( + sprintf('networks/%s/connect', $network) + ); + $jsonPayload = ['Container' => $id]; + if ($alias !== '') { + $jsonPayload['EndpointConfig'] = ['Aliases' => [$alias]]; + } + + try { + $this->guzzleClient->request( + 'POST', + $url, + [ + 'json' => $jsonPayload ] ); } catch (RequestException $e) { @@ -641,18 +828,35 @@ class DockerActionManager } } - public function ConnectMasterContainerToNetwork() : void - { - $this->ConnectContainerIdToNetwork('nextcloud-aio-mastercontainer'); + public function ConnectMasterContainerToNetwork(): void { + $this->ConnectContainerIdToNetwork('nextcloud-aio-mastercontainer', ''); + // Don't disconnect here since it slows down the initial login by a lot. Is getting done during cron.sh instead. + // $this->DisconnectContainerFromBridgeNetwork('nextcloud-aio-mastercontainer'); } - public function ConnectContainerToNetwork(Container $container) : void - { - $this->ConnectContainerIdToNetwork($container->GetIdentifier()); + public function ConnectContainerToNetwork(Container $container): void { + // Add a secondary alias for domaincheck container, to keep it as similar to actual apache controller as possible. + // If a reverse-proxy is relying on container name as hostname this allows it to operate as usual and still validate the domain + // The domaincheck container and apache container are never supposed to be active at the same time because they use the same APACHE_PORT anyway, so this doesn't add any new constraints. + $alias = ($container->identifier === 'nextcloud-aio-domaincheck') ? 'nextcloud-aio-apache' : ''; + + $this->ConnectContainerIdToNetwork($container->identifier, $container->internalPorts, alias: $alias); + + if ($container->identifier === 'nextcloud-aio-apache' || $container->identifier === 'nextcloud-aio-domaincheck') { + $apacheAdditionalNetwork = $this->configurationManager->getApacheAdditionalNetwork(); + if ($apacheAdditionalNetwork !== '') { + $this->ConnectContainerIdToNetwork($container->identifier, $container->internalPorts, $apacheAdditionalNetwork, false, $alias); + } + } } - public function StopContainer(Container $container) : void { - $url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->GetIdentifier()), $container->GetMaxShutdownTime())); + public function StopContainer(Container $container, bool $forceStopContainer = false): void { + if ($forceStopContainer) { + $maxShutDownTime = 10; + } else { + $maxShutDownTime = $container->maxShutdownTime; + } + $url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->identifier), $maxShutDownTime)); try { $this->guzzleClient->post($url); } catch (RequestException $e) { @@ -662,8 +866,7 @@ class DockerActionManager } } - public function GetBackupcontainerExitCode() : int - { + public function GetBackupcontainerExitCode(): int { $containerName = 'nextcloud-aio-borgbackup'; $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); try { @@ -675,7 +878,7 @@ class DockerActionManager throw $e; } - $responseBody = json_decode((string)$response->getBody(), true); + $responseBody = json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR); $exitCode = $responseBody['State']['ExitCode']; if (is_int($exitCode)) { @@ -685,8 +888,7 @@ class DockerActionManager } } - public function GetDatabasecontainerExitCode() : int - { + public function GetDatabasecontainerExitCode(): int { $containerName = 'nextcloud-aio-database'; $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); try { @@ -698,7 +900,7 @@ class DockerActionManager throw $e; } - $responseBody = json_decode((string)$response->getBody(), true); + $responseBody = json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR); $exitCode = $responseBody['State']['ExitCode']; if (is_int($exitCode)) { @@ -708,21 +910,70 @@ class DockerActionManager } } - public function isLoginAllowed() : bool { + public function isLoginAllowed(): bool { $id = 'nextcloud-aio-apache'; $apacheContainer = $this->containerDefinitionFetcher->GetContainerById($id); - if ($this->GetContainerStartingState($apacheContainer) instanceof RunningState) { + if ($this->GetContainerStartingState($apacheContainer) === ContainerState::Running) { return false; } return true; } - public function isBackupContainerRunning() : bool { + public function isBackupContainerRunning(): bool { $id = 'nextcloud-aio-borgbackup'; $backupContainer = $this->containerDefinitionFetcher->GetContainerById($id); - if ($this->GetContainerRunningState($backupContainer) instanceof RunningState) { + if ($this->GetContainerRunningState($backupContainer) === ContainerState::Running) { return true; } return false; } + + private function GetCreatedTimeOfNextcloudImage(string $imageName): ?string { + $imageName = $imageName . ':' . $this->GetCurrentChannel(); + try { + $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName)); + $imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); + + if (!isset($imageOutput['Created'])) { + error_log('Created is not set of image ' . $imageName); + return null; + } + + return str_replace('T', ' ', (string)$imageOutput['Created']); + } catch (\Exception $e) { + return null; + } + } + + public function GetAndGenerateSecretWrapper(string $secretId): string { + return $this->configurationManager->getAndGenerateSecret($secretId); + } + + public function isNextcloudImageOutdated(): bool { + $createdTime = $this->GetCreatedTimeOfNextcloudImage('ghcr.io/nextcloud-releases/aio-nextcloud'); + + if ($createdTime === null) { + $createdTime = $this->GetCreatedTimeOfNextcloudImage('nextcloud/aio-nextcloud'); + } + + if ($createdTime === null) { + return false; + } + + // If the image is older than 90 days, it is outdated. + if ((time() - (60 * 60 * 24 * 90)) > strtotime($createdTime)) { + return true; + } + + return false; + } + + public function GetLatestDigestOfTag(string $imageName, string $tag): ?string { + $prefix = 'ghcr.io/'; + if (str_starts_with($imageName, $prefix)) { + return $this->gitHubContainerRegistryManager->GetLatestDigestOfTag(str_replace($prefix, '', $imageName), $tag); + } else { + return $this->dockerHubManager->GetLatestDigestOfTag($imageName, $tag); + } + } } diff --git a/php/src/Docker/DockerHubManager.php b/php/src/Docker/DockerHubManager.php index c75c5efe..256d592e 100644 --- a/php/src/Docker/DockerHubManager.php +++ b/php/src/Docker/DockerHubManager.php @@ -6,12 +6,11 @@ use AIO\ContainerDefinitionFetcher; use AIO\Data\ConfigurationManager; use GuzzleHttp\Client; -class DockerHubManager -{ +readonly class DockerHubManager { private Client $guzzleClient; - public function __construct() - { + public function __construct( + ) { $this->guzzleClient = new Client(); } @@ -31,15 +30,15 @@ class DockerHubManager 'https://auth.docker.io/token?service=registry.docker.io&scope=repository:' . $name . ':pull' ); $body = $authTokenRequest->getBody()->getContents(); - $decodedBody = json_decode($body, true); + $decodedBody = json_decode($body, true, 512, JSON_THROW_ON_ERROR); if(isset($decodedBody['token'])) { $authToken = $decodedBody['token']; $manifestRequest = $this->guzzleClient->request( - 'GET', + 'HEAD', 'https://registry-1.docker.io/v2/'.$name.'/manifests/' . $tag, [ 'headers' => [ - 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json', + 'Accept' => 'application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json', 'Authorization' => 'Bearer ' . $authToken, ], ] @@ -59,4 +58,4 @@ class DockerHubManager return null; } } -} \ No newline at end of file +} diff --git a/php/src/Docker/GitHubContainerRegistryManager.php b/php/src/Docker/GitHubContainerRegistryManager.php new file mode 100644 index 00000000..eeecfb28 --- /dev/null +++ b/php/src/Docker/GitHubContainerRegistryManager.php @@ -0,0 +1,62 @@ +guzzleClient = new Client(); + } + + public function GetLatestDigestOfTag(string $name, string $tag): ?string + { + $cacheKey = 'ghcr-manifest-' . $name . $tag; + + $cachedVersion = apcu_fetch($cacheKey); + if ($cachedVersion !== false && is_string($cachedVersion)) { + return $cachedVersion; + } + + // If one of the links below should ever become outdated, we can still upgrade the mastercontainer via the webinterface manually by opening '/api/docker/getwatchtower' + + try { + $authTokenRequest = $this->guzzleClient->request( + 'GET', + 'https://ghcr.io/token?scope=repository:' . $name . ':pull' + ); + $body = $authTokenRequest->getBody()->getContents(); + $decodedBody = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + if (isset($decodedBody['token'])) { + $authToken = $decodedBody['token']; + $manifestRequest = $this->guzzleClient->request( + 'HEAD', + 'https://ghcr.io/v2/' . $name . '/manifests/' . $tag, + [ + 'headers' => [ + 'Accept' => 'application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json', + 'Authorization' => 'Bearer ' . $authToken, + ], + ] + ); + $responseHeaders = $manifestRequest->getHeader('docker-content-digest'); + if (count($responseHeaders) === 1) { + $latestVersion = $responseHeaders[0]; + apcu_add($cacheKey, $latestVersion, 600); + return $latestVersion; + } + } + + error_log('Could not get digest of container ' . $name . ':' . $tag); + return null; + } catch (\Exception $e) { + error_log('Could not get digest of container ' . $name . ':' . $tag . ' ' . $e->getMessage()); + return null; + } + } +} diff --git a/php/src/Middleware/AuthMiddleware.php b/php/src/Middleware/AuthMiddleware.php index 98e4f7d4..724f1776 100644 --- a/php/src/Middleware/AuthMiddleware.php +++ b/php/src/Middleware/AuthMiddleware.php @@ -8,12 +8,10 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -class AuthMiddleware -{ - private AuthManager $authManager; - - public function __construct(AuthManager $authManager) { - $this->authManager = $authManager; +readonly class AuthMiddleware { + public function __construct( + private AuthManager $authManager + ) { } public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface @@ -28,10 +26,27 @@ class AuthMiddleware if(!in_array($request->getUri()->getPath(), $publicRoutes)) { if(!$this->authManager->IsAuthenticated()) { - $response = new Response(); - return $response - ->withHeader('Location', '/') - ->withStatus(302); + $status = 302; + + // Check the url of the request: split the string by '/' and count the number of elements + // Note that the path that gets to this middleware is not aware of any base path managed by a reverse proxy, so if the url is 'https://example.com/AIO/somepage', the path will be 'https://mastercontainer/somepage' + if (count(explode('/', $request->getUri()->getPath())) < 2) { + // If there are less than 2 elements it means we are somewhere in the root folder (no '/', so no subfolder), so we redirect to the same folder level to offload the redirection to the appropriate page to 'index.php' (specifically, once in the root level the login page will be loaded since we are not authenticated) + $location = '.'; + } else { + // If there are 2 or more elements it means we are in a subfolder, so we need to go back to the root folder + // In the best case we need to go back by 1 level only + $location = '..'; + // In the worst case we need to go back by n levels, where n is the number of elements - 2 (the first element is not a folder, the second element is already accounted for by the initial '..') + for ($i = 1; $i < count(explode('/', $request->getUri()->getPath())) - 2; $i++) { + // For each extra level we need to go back by another level + $location = $location . '/..'; + } + } + + $headers = ['Location' => $location]; + $response = new Response($status, $headers); + return $response; } } diff --git a/php/src/Twig/ClassExtension.php b/php/src/Twig/ClassExtension.php index 9204aee1..7f478994 100644 --- a/php/src/Twig/ClassExtension.php +++ b/php/src/Twig/ClassExtension.php @@ -7,6 +7,7 @@ use Twig\TwigFunction; class ClassExtension extends TwigExtension { + #[\Override] public function getFunctions() : array { return array( @@ -14,7 +15,7 @@ class ClassExtension extends TwigExtension ); } - public function getClassName($object) : ?string + public function getClassName(mixed $object) : ?string { if (!is_object($object)) { return null; diff --git a/php/src/Twig/CsrfExtension.php b/php/src/Twig/CsrfExtension.php index 6df974ae..51334864 100644 --- a/php/src/Twig/CsrfExtension.php +++ b/php/src/Twig/CsrfExtension.php @@ -3,19 +3,16 @@ namespace AIO\Twig; use Slim\Csrf\Guard; +use Twig\Extension\AbstractExtension; +use Twig\Extension\GlobalsInterface; -class CsrfExtension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface -{ - /** - * @var Guard - */ - protected Guard $csrf; - - public function __construct(Guard $csrf) - { - $this->csrf = $csrf; +class CsrfExtension extends AbstractExtension implements GlobalsInterface { + public function __construct( + protected Guard $csrf + ) { } + #[\Override] public function getGlobals() : array { // CSRF token name and value @@ -35,4 +32,4 @@ class CsrfExtension extends \Twig\Extension\AbstractExtension implements \Twig\E ] ]; } -} \ No newline at end of file +} diff --git a/php/templates/already-installed.twig b/php/templates/already-installed.twig index 708128a8..e16e6792 100644 --- a/php/templates/already-installed.twig +++ b/php/templates/already-installed.twig @@ -1,5 +1,13 @@ {% extends "layout.twig" %} {% block body %} - Already installed. + {% endblock %} diff --git a/php/templates/components/container-state.twig b/php/templates/components/container-state.twig new file mode 100644 index 00000000..07580e66 --- /dev/null +++ b/php/templates/components/container-state.twig @@ -0,0 +1,27 @@ +{# @var c \App\Containers\Container #} +
  • + + {% if c.GetStartingState().value == 'starting' %} + + {{ c.displayName }} + (Starting) + {% elseif c.GetRunningState().value == 'running' %} + + {{ c.displayName }} + (Running) + {% else %} + + {{ c.displayName }} + (Stopped) + {% endif %} + {% if c.documentation != '' %} + (docs) + {% endif %} + + {% if c.GetUiSecret() != '' %} +
    + Show password for {{ c.displayName }} + +
    + {% endif %} +
  • \ No newline at end of file diff --git a/php/templates/containers.twig b/php/templates/containers.twig index cde78a36..8e437bc2 100644 --- a/php/templates/containers.twig +++ b/php/templates/containers.twig @@ -5,564 +5,639 @@
    -
    - -
    -
    + + - +
    -
    -

    Nextcloud AIO v2.0.0

    +
    +
    + {% set aio_version = include('includes/aio-version.twig') %} +

    Nextcloud AIO v{{ aio_version }}

    - {% set isAnyRunning = false %} - {% set isAnyRestarting = false %} - {% set isWatchtowerRunning = false %} - {% set isRestoreRunning = false %} - {% set isBackupOrRestoreRunning = false %} - {% set isApacheStarting = false %} + {# Add 2nd tab warning #} + - {% if is_backup_container_running == true %} - {% if borg_backup_mode == 'restore' %} - {% set isRestoreRunning = true %} - {% endif %} - {% if borg_backup_mode == 'backup' or borg_backup_mode == 'restore' %} - {% set isBackupOrRestoreRunning = true %} - {% endif %} - {% endif %} + {# timezone-prefill #} + - {% for container in containers %} - {% if container.GetIdentifier() not in ['nextcloud-aio-domaincheck', 'nextcloud-aio-borgbackup', 'nextcloud-aio-watchtower'] and class(container.GetRunningState()) == 'AIO\\Container\\State\\RunningState' %} - {% set isAnyRunning = true %} - {% endif %} - {% if container.GetIdentifier() not in ['nextcloud-aio-domaincheck', 'nextcloud-aio-borgbackup', 'nextcloud-aio-watchtower'] and class(container.GetRestartingState()) == 'AIO\\Container\\State\\RestartingState' %} - {% set isAnyRestarting = true %} - {% endif %} - {% if container.GetIdentifier() == 'nextcloud-aio-watchtower' and class(container.GetRunningState()) == 'AIO\\Container\\State\\RunningState' %} - {% set isWatchtowerRunning = true %} - {% endif %} - {% if container.GetIdentifier() == 'nextcloud-aio-apache' and class(container.GetStartingState()) == 'AIO\\Container\\State\\StartingState' %} - {% set isApacheStarting = true %} - {% endif %} - {% endfor %} + {# js for optional containers and additional containers forms #} + - {% if is_daily_backup_running == true %} - Daily backup currently running. (Logs)

    - {% if automatic_updates == true %} - It will update your containers, the mastercontainer and on saturdays your Nextcloud apps if the backup is successful.

    - {% if is_mastercontainer_update_available == true %} - Since the mastercontainer gets updated, it will restart the container which will make it unavailable for a moment. (Logs)

    - {% endif %} - {% endif %} - {% if has_update_available == false %} - The whole process should not take more than a few minutes.

    - {% elseif automatic_updates == true %} - The whole process can take a while because your containers get updated.

    - {% endif %} - Reload ↻
    - {% elseif isWatchtowerRunning == true %} - Mastercontainer update currently running. It will restart the mastercontainer soon which will make it unavailable for a moment. Please wait until that's done. (Logs)

    - Reload ↻
    - {% else %} - {% if is_backup_container_running == false and domain == "" %} - {% if is_mastercontainer_update_available == true %} -

    Mastercontainer update

    - ⚠️ A mastercontainer update is available. Please click on the button below to update it. Afterwards, you will be able to proceed with the setup.

    -
    - - - -
    - {% else %} - {% if borg_backup_host_location == '' and borg_restore_password == '' %} - Nextcloud AIO stands for Nextcloud All In One and provides easy deployment and maintenance with most features included in this one Nextcloud instance.

    -

    New AIO instance

    - Please type in the domain that will be used for Nextcloud if you want to create a new instance:

    - {% if skip_domain_validation == true %} - Please note: The domain validation is disabled so any domain will be accepted here! So make sure that you do not make a typo here as you will not be able to change it afterwards!

    - {% endif %} -
    - - - - -
    - {% if skip_domain_validation == false %} - Make sure that this server is reachable on Port 443 and you've correctly set up the DNS config for the domain that you enter.

    - If you have a dynamic IP-address, you can use e.g. DDclient with a compatible domain provider for DNS updates.

    - Hint: If the domain validation fails but you are completely sure that you've configured everything correctly, you may skip the domain validation by following this documentation.

    - {% endif %} - -

    Restore former AIO instance from backup

    - You can alternatively restore a former AIO instance from backup.

    - {% endif %} - - {% if is_instance_restore_attempt == false %} - {% if borg_backup_host_location != '' and borg_restore_password != '' %} - {% if borg_backup_mode in ['test', 'check'] %} - {% if backup_exit_code > 0 %} - Last {{ borg_backup_mode }} failed! (Logs)

    - {% if borg_backup_mode == 'test' %} - Please adjust the path and/or the password in order to make it work!

    - {% elseif borg_backup_mode == 'check' %} - The backup archive seems to be corrupt. Please try to use a different intact backup archive or try to fix it by following this documentation - {% endif %} - {% elseif backup_exit_code == 0 %} - Last {{ borg_backup_mode }} successful! (Logs)

    - {% if borg_backup_mode == 'test' %} - Feel free to check the integrity of the backup archive below before starting the restore process in order to make double-sure that the restore will work. This can take a long time though depending on the size of the backup archive and is thus not required.

    -
    - - -
    -
    - {% endif %} - Choose the backup that you want to restore and click on the button below to restore the selected backup. This will restore the whole AIO instance from backup. Please not that the current AIO password will be kept and the AIO password not restored from backup!

    -
    - - - - -
    - {% endif %} - {% elseif borg_backup_mode == 'restore' %} - {% if backup_exit_code > 0 %} - Last restore failed! (Logs)

    - Somehow the restore failed which is unexpected! Please adjust the path and password, test it and try to restore again! - {% endif %} - {% endif %} - {% endif %} - - {% if borg_backup_host_location == '' or borg_restore_password == '' or borg_backup_mode not in ['test', 'check', ''] or backup_exit_code > 0 %} - Please enter the location of the backup archive on your host and the password of the backup archive below:

    -
    - - - - - -
    - The folder path that you enter must start with / and must not end with /.

    - An example for Linux is /mnt/backup.
    - For macOS it may be /var/backup.
    - On Windows it must be nextcloud_aio_backupdir. You need to create the 'nextcloud_aio_backupdir' volume beforehand by following this documentation: click here

    - ⚠️ Please note that the backup archive must be located in a subfolder of the folder that you enter here and the subfolder which contains the archive must be named 'borg'. Otherwise will the backup container not find the backup archive!

    - {% endif %} - {% else %} - Everything set! Click on the button below to test the path and password:

    -
    - - -
    -
    - {% endif %} - {% endif %} - {% endif %} - - {% if domain != "" and was_start_button_clicked == true %} - You are running the {{ current_channel }} channel. (Logs)

    - {% endif %} + {% set hasBackupLocation = borg_backup_host_location or borg_remote_repo %} + {% set isAnyRunning = false %} + {% set isAnyRestarting = false %} + {% set isWatchtowerRunning = false %} + {% set isDomaincheckRunning = false %} + {% set isBackupOrRestoreRunning = false %} + {% set isApacheStarting = false %} + {# Setting newMajorVersion to '' will hide corresponding options/elements, can be set to an integer like 26 in order to show corresponding elements. If set, also increase installLatestMajor in https://github.com/nextcloud/all-in-one/blob/main/php/src/Controller/DockerController.php #} + {% set newMajorVersionString = '' %} {% if is_backup_container_running == true %} - Backup container is currently running. (Logs)

    - Reload ↻

    + {% if borg_backup_mode == 'backup' or borg_backup_mode == 'restore' %} + {% set isBackupOrRestoreRunning = true %} + {% endif %} {% endif %} - {% if domain != "" %} - {% if isAnyRunning == true %} - {% if isApacheStarting != true %} - {% if borg_backup_host_location != '' %} -
    - Click here to reveal the initial Nextcloud credentials
    - {% endif %} - Initial Nextcloud username: admin
    - Initial Nextcloud password: - {% if borg_backup_host_location != '' %} - {# nextcloud_password needs to be duplicated due to a bug in Firefox. See https://github.com/nextcloud/all-in-one/issues/638. #} - {{ nextcloud_password }}


    - {% else %} - {{ nextcloud_password }}

    - {% endif %} - Open your Nextcloud ↗
    - {% else %} - {% if isAnyRestarting == false %} - Containers are currently starting.

    - Reload ↻

    - {% else %} - It seems like at least one container is currently restarting which means it is not able to start correctly.

    - To break out this endless loop, you can stop the containers below and investigate the issue by having a look at the container logs before starting them again.

    -
    - - - -
    - {% endif %} + {% for container in containers %} + {% if container.displayName != '' and container.GetRunningState().value == 'running' %} + {% set isAnyRunning = true %} + {% endif %} + {% if container.displayName != '' and container.GetRestartingState().value == 'restarting' %} + {% set isAnyRestarting = true %} + {% endif %} + {% if container.identifier == 'nextcloud-aio-watchtower' and container.GetRunningState().value == 'running' %} + {% set isWatchtowerRunning = true %} + {% endif %} + {% if container.identifier == 'nextcloud-aio-domaincheck' and container.GetRunningState().value == 'running' %} + {% set isDomaincheckRunning = true %} + {% endif %} + {% if container.identifier == 'nextcloud-aio-apache' and container.GetStartingState().value == 'starting' %} + {% set isApacheStarting = true %} + {% endif %} + {% endfor %} + + {% if is_daily_backup_running == true %} +

    Daily backup currently running. (Mastercontainer logs) (Borg backup container logs)

    + {% if automatic_updates == true %} +

    This will update your containers, the mastercontainer and, on Saturdays, your Nextcloud apps if the backup is successful.

    + {% if is_mastercontainer_update_available == true %} +

    When the mastercontainer is updated it will restart, making it unavailable for a moment. (Logs)

    {% endif %} {% endif %} - - {% if was_start_button_clicked == true %} -

    Containers

    -
      - {# @var containers \AIO\Container\Container[] #} - {% for container in containers %} - {% if container.GetIdentifier() not in ['nextcloud-aio-borgbackup', 'nextcloud-aio-watchtower', 'nextcloud-aio-domaincheck'] %} -
    • - {% if class(container.GetStartingState()) == 'AIO\\Container\\State\\StartingState' %} - - {{container.GetDisplayName()}} (Starting) - {% elseif class(container.GetRunningState()) == 'AIO\\Container\\State\\RunningState' %} - - {{container.GetDisplayName()}} (Running) - {% else %} - - {{container.GetDisplayName()}} (Stopped) - {% endif %} -
    • - {% endif %} - {% endfor %} -
    - - {% if has_update_available == true %} - {% if is_mastercontainer_update_available == false %} - ⚠️ Container updates are available. Click on 'Stop Containers' and 'Start Containers' to update them. You should consider creating a backup first.

    - {% endif %} - {% else %} - {% if is_mastercontainer_update_available == false %} - Your containers are up-to-date.

    - {% endif %} - {% endif %} + {% if has_update_available == false %} +

    The whole process should not take more than a few minutes.

    + {% elseif automatic_updates == true %} +

    The whole process can take a while as your containers will be updated.

    {% endif %} - - {% if isAnyRunning == true %} - {% if isApacheStarting != true %} - {% if is_mastercontainer_update_available == true %} - ⚠️ A mastercontainer update is available. Please click on the button below to stop your containers in order to be able to update the mastercontainer.

    - {% if current_channel starts with 'latest' %} - You can find the changelog here

    - {% elseif current_channel starts with 'beta' %} - You can find the changelog here

    - {% elseif current_channel starts with 'develop' %} - You can find all changes here

    - {% endif %} - {% endif %} -
    +

    Reload ↻

    +

    If the daily backup is stuck somehow, you can unstick it by running sudo docker exec nextcloud-aio-mastercontainer rm /mnt/docker-aio-config/data/daily_backup_running and afterwards reloading this interface.

    + {% elseif isWatchtowerRunning == true %} +

    Mastercontainer update currently running. Once the update is complete the mastercontainer will restart, making it unavailable for a moment. Please wait until it's done. (Logs)

    +

    Reload ↻

    + {% else %} + {% if is_backup_container_running == false and domain == "" %} + {% if isDomaincheckRunning == false %} +

    Domaincheck container is not running

    +

    This is not expected. Most likely this happened because port {{ apache_port }} is already in use on your server. You can check the mastercontainer logs and domaincheck container logs for further clues. You should be able to resolve this by adjusting the APACHE_PORT by following the reverse proxy documentation. Advice: have a detailed look at the changed docker run command for AIO.

    + {% elseif is_mastercontainer_update_available == true %} +

    Mastercontainer update

    +

    ⚠️ A mastercontainer update is available. Please click on the button below to update it. Afterwards, you will be able to proceed with the setup.

    + - +
    - {% endif %} - {% else %} - {% if isRestoreRunning == true %} - Restore currently running. Cannot start the containers until that's done.

    - {% elseif has_update_available == true and isBackupOrRestoreRunning == true %} - Restore or Backup currently running and container update available. Cannot start the containers until that's done.

    {% else %} - {% if was_start_button_clicked == false %} - Clicking on the button below will download all docker containers and start them. This can take a lot of time depending on your internect connection. Since the overall size is a few GB, this will take around 5-10 min or more. So be aware and patient!

    - {% endif %} - {% if is_mastercontainer_update_available == true %} - ⚠️ A mastercontainer update is available. Please click on the button below to update it.

    -
    - - - -
    - {% else %} - {% if was_start_button_clicked == false or has_update_available == false %} -
    - - - -
    + {% if not hasBackupLocation %} +

    The official Nextcloud installation method. Nextcloud All-in-One provides easy deployment and maintenance with most features included in this one Nextcloud instance.

    +

    You can either create a new AIO instance or restore a former AIO instance from backup. See the two sections below.

    + {{ include('includes/aio-config.twig') }} +

    New AIO instance

    + {% if apache_port == '443' %} +

    AIO is currently in "normal mode" which means that it handles the TLS proxying itself. This also means that it cannot be installed behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). If you want to run AIO behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else), see the reverse proxy documentation. Advice: have a detailed look at the changed docker run command for AIO.

    {% else %} -
    - - - -
    +

    AIO is currently in "reverse proxy mode" which means that it can be installed behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) and does not do the TLS proxying itself.

    {% endif %} - {% endif %} - {% endif %} - {% endif %} - - {% if was_start_button_clicked == true %} - - {% if is_backup_section_enabled == false %} -

    Backup and restore

    - The backup section is disabled via environmental variable.

    - {% else %} - {% if is_backup_container_running == false and borg_backup_host_location == "" and isApacheStarting != true %} -

    Backup and restore

    - Please type in the directory where backups will get created on the host system:

    -
    - +

    Please type in the domain that will be used for Nextcloud and submit it.

    + {% if skip_domain_validation == true %} +

    Please note: The domain validation is disabled so any domain will be accepted here! Make sure you do not make a typo here as you will not be able to change it afterwards!

    + {% endif %} + + - -
    - The folder path that you enter must start with / and must not end with /.

    - An example for Linux is /mnt/backup.
    - For macOS it may be /var/backup.
    - On Windows it must be nextcloud_aio_backupdir. You need to create the 'nextcloud_aio_backupdir' volume beforehand by following this documentation: click here

    - {% endif %} - {% endif %} - - {% if is_backup_section_enabled == true %} - - {% if borg_backup_host_location != "" %} - {% if is_backup_container_running == false %} -

    Backup and restore

    - {% if backup_exit_code > 0 %} - Last {{ borg_backup_mode }} failed! (Logs)

    - {% if has_backup_run_once == false %} - You may change the backup path again since the initial backup was not successful. After submitting the new value, you need to click on 'Create Backup' for testing the new value.

    -
    - - - - -
    - {% endif %} - {% elseif backup_exit_code == 0 %} - {% if borg_backup_mode == "backup" %} - Last {{ borg_backup_mode }} successful on {{ last_backup_time }} UTC! (Logs)

    - {% else %} - Last {{ borg_backup_mode }} successful! (Logs)

    - {% endif %} + {% if skip_domain_validation == true %} + {% endif %} + + + {% if skip_domain_validation == false %} +

    Make sure that this server is reachable on port 443 (port 443/tcp is open/forwarded in your firewall/router and 443/udp as well if you want to enable http3) and that you've correctly set up the DNS config for the domain that you enter (set the A record to your public ipv4-address and if you need ipv6, set the AAAA record to your public ipv6-address. A CNAME record is, of course, also possible). You should see hints on what went wrong in the top right corner if your domain is not accepted.

    +
    + Click here for further hints +

    If you do not have a domain yet, you can get one for free e.g. from duckdns.org and others. Recommended is to use Tailscale

    +

    If you have a dynamic public IP-address, you can use e.g. DDclient with a compatible domain provider for DNS updates.

    +

    If you only want to install AIO locally without exposing it to the public internet or if you cannot do so, feel free to follow this documentation.

    +

    If you should be using Cloudflare Proxy for your domain, make sure to disable the Proxy feature temporarily as it might block the domain validation attempts.

    + {% if apache_port != '443' %} +

    If you run into issues with your domain being accepted, see these steps for how to debug things.

    + {% endif %} +

    Hint: If the domain validation fails but you are completely sure that you've configured everything correctly, you may skip the domain validation by following this documentation.

    +
    {% endif %} - {% if is_backup_container_running == false and isApacheStarting == false %} - {% if has_backup_run_once == true %} -
    - Click here to reveal all backup options (it also includes an option for automatic updates)
    - {% endif %} -

    Backup information

    - This is your encryption password for backups: {{ borgbackup_password }}

    - Please save it at a safe place since you won't be able to restore from backup if you lose this password!

    - Backed up will get all important data of your Nextcloud AIO instance like the database, your files and configuration files of the mastercontainer and else.

    - The backup itself will use a tool that is called BorgBackup which is a well-known server backup tool that efficiently backs up your files and encrypts them on the fly.

    - Backups get created in the following directory on the host: {{ borg_backup_host_location }}/borg

    - Be aware that this solution does not back up files and folders that are mounted into Nextcloud using the external storage app. +

    Restore former AIO instance from backup

    +

    You can alternatively restore a former AIO instance from backup.

    + {% endif %} - {% if isApacheStarting != true %} -

    Backup creation

    - Clicking on the button below will create a backup.

    -
    - - - -
    - - {% if has_backup_run_once == true %} -

    Backup check

    - Click on the button below to perform a backup integrity check. This is an option that verifies that your backup is intact but it should't be needed in most situtations.

    -
    - - -
    -
    - -

    Backup restore

    - Choose the backup that you want to restore and click on the button below to restore the selected backup. This will overwrite all your files with the state of the backup so you should consider creating a backup first. It also makes sense to run an integrity check before restoring your files but is not mandatory since it shouldn't be needed in most situations. Please note that this will not restore additionally chosen backup directories!

    -
    + {% if is_instance_restore_attempt == false %} + {% if hasBackupLocation %} + {% if borg_backup_mode in ['test', 'check'] %} + {% if backup_exit_code > 0 %} +

    Last {{ borg_backup_mode }} failed! (Logs)

    + {% if borg_backup_mode == 'test' %} +

    Please adjust the path and/or the encryption password in order to make it work!

    + {% elseif borg_backup_mode == 'check' %} +

    The backup archive seems to be corrupt. Please try to use a different intact backup archive or try to fix it by following this documentation

    +
    + Reveal repair option +

    Below is the option to repair the integrity of your backup. Please note: Please only use this after you have read the documentation above! (It will run the command 'borg check --repair' for you.)

    + + + + + +
    + {% endif %} + {% elseif backup_exit_code == 0 %} +

    Last {{ borg_backup_mode }} successful! (Logs)

    + {% if borg_backup_mode == 'test' %} +

    Feel free to check the integrity of the backup archive below before starting the restore process in order to make ensure that the restore will work. This can take a long time though depending on the size of the backup archive and is thus not required.

    +
    + + + +
    + {% endif %} +

    Choose the backup that you want to restore and click on the button below to restore the selected backup. This will restore the whole AIO instance. Please note that the current AIO passphrase will be kept and the previous AIO passphrase will not be restored from backup!

    +

    Important: If the backup that you want to restore contained any community container, you need to restore the same backup a second time after this attempt so that the community container data is also correctly restored.

    +
    - +
    +
    +
    - -

    Daily backup and automatic updates

    - {% if daily_backup_time == "" %} - By entering a time below, you can enable daily backups. It will create them at the entered time in 24h format. E.g. 04:00 will create backups at 4 am UTC and 16:00 at 4 pm UTC.

    -
    - - - -
    -
    -
    - {% else %} - Daily backups will be created at {{ daily_backup_time }} UTC which includes a notification about the result of the backup. - {% if automatic_updates == true %} - Also your containers, the mastercontainer and on saturdays your Nextcloud apps will be automatically updated. - {% endif %} - You can disable this option again by clicking on the button below.

    -
    - - - - -
    - {% endif %} - -

    Back up additional directories and docker volumes of your host

    - Below, you can enter directories and docker volumes of your host that will backed up additionally into the same borg backup archive.

    -
    - - - -
    -
    - Each line and entry needs to start with a slash or letter/digit. Allowed are only a-z, A-Z, 0-9, _, -, and /. If the entry begins with a letter/digit are slashes not supported. Two valid entries are /directory/on/the/host and my_custom_docker_volume. You need to make sure yourself that all given directories exist. Otherwise the backup container will fail starting!

    - Make sure to specify all storages that you want to back up separately since storages will not be mounted recursively. E.g. providing / as additional backup directory will only back up files and folders that are stored on the root partition and not on the EFI partition or any other. Excluded by the backup will be caches and a few other directories. You should make sure to stop all services before the backup can run correctly if you want to back up the root partition. For automating this see this documentation

    - Please note that the chosen directories/volumes will not be restored when you restore your instance, so this would need to be done manually.

    - {% if additional_backup_directories != "" %} - This option is currently set. You can disable it again by clearing the field and submitting your changes.

    - {% endif %} + {% endif %} + {% elseif borg_backup_mode == 'restore' %} + {% if backup_exit_code > 0 %} +

    Last restore failed! (Logs)

    +

    The restore process has unexpectedly failed! Please adjust the path and encryption password, test it and try to restore again!

    {% endif %} {% endif %} - {% if has_backup_run_once == false %} -

    - {% else %} -


    + {% endif %} + + {% if not hasBackupLocation or borg_backup_mode not in ['test', 'check', ''] or backup_exit_code > 0 %} + {% if borg_remote_repo and backup_exit_code > 0 %} +

    + You may still need to authorize this pubkey on your borg remote:
    {{ borg_public_key }}
    + To try again, resubmit your location and rerun the test. +

    + {% endif %} + +

    + Please enter the location of the backup archive on your host or a + remote borg repo url + if stored remotely; and the encryption password of the backup archive below and submit all values: +

    +
    +
    +
    +
    + + + +
    + {{ include('includes/backup-dirs.twig') }} +

    ⚠️ Please note that the backup archive must be located in a subfolder of the folder that you enter here and the subfolder which contains the archive must be named 'borg', or the backup container will not be able to find the backup archive!

    + {% endif %} + {% else %} +

    Everything set! Click on the button below to test the path and encryption password:

    +
    + + + +
    + {% endif %} + {% endif %} +

    How to reset the AIO instance?

    +

    If something should be going wrong, for example during the initial installation, you can reset the instance by following this documentation.

    + {% endif %} + + {% if was_start_button_clicked == true %} + {% if current_channel starts with 'latest' or current_channel starts with 'beta' or current_channel starts with 'develop' %} +

    You are running the {{ current_channel }} channel. (Logs)

    + {% else %} +

    No channel was found. This means that AIO is not able to update itself and its component and will also not be able to report about updates. Updates need to be done externally.

    + {% endif %} + {% endif %} + + {% if is_backup_container_running == true %} +

    Backup container is currently running: {{ borg_backup_mode }} (Logs)

    +

    Reload ↻

    + {% endif %} + + {% if domain != "" %} + {% if isAnyRunning == true %} + {% if isApacheStarting != true %} + {% if hasBackupLocation %} +
    + Click here to reveal the initial Nextcloud credentials + {% endif %} +

    Initial Nextcloud username: admin

    + {% if hasBackupLocation %} + {# nextcloud_password needs to be duplicated due to a bug in Firefox. See https://github.com/nextcloud/all-in-one/issues/638. #} +

    Initial Nextcloud password: {{ nextcloud_password }}

    + {% else %} +

    Initial Nextcloud password: {{ nextcloud_password }}

    + {% endif %} +

    Open your Nextcloud ↗

    + {% if not hasBackupLocation %} +

    If your Nextcloud does not open when clicking the button above, see this documentation

    + {% endif %} + {% else %} + {% if isAnyRestarting == false %} +

    Containers are currently starting. You might inspect the container logs by clicking on Starting next to each container for further details.

    +

    Reload ↻

    + {% else %} +

    It seems at least one container was not able to start correctly and is currently restarting.

    +

    To break this endless loop, you can stop the containers below and investigate the issue in the container logs before starting the containers again.

    +
    + + + +
    + {% endif %} + {% endif %} + {% endif %} + + {% if isApacheStarting == false and is_backup_container_running == false %} + {{ include('includes/aio-config.twig') }} + {% endif %} + + {% if was_start_button_clicked == true %} +

    Containers

    +
      + {# @var containers \AIO\Container\Container[] #} + {% for container in containers %} + {% if container.displayName != '' %} + {% include 'components/container-state.twig' with {'c': container} only %} + {% endif %} + {% endfor %} +
    + + {% if has_update_available == true %} + {% if is_mastercontainer_update_available == false %} +

    ⚠️ Container updates are available. Click on Stop containers and Start and update containers to update them. You should consider creating a backup first.

    + {% endif %} + {% else %} + {% if is_mastercontainer_update_available == false %} +

    Your containers are up-to-date.

    + {% if newMajorVersionString != '' and isAnyRunning == true and isApacheStarting != true %} +
    + Note about Nextcloud Hub {{ newMajorVersionString }} +

    If you haven't upgraded to Nextcloud Hub {{ newMajorVersionString }} yet and want to do that now, feel free to follow this documentation

    +
    {% endif %} {% endif %} {% endif %} {% endif %} - {% if is_backup_container_running == false %} - {% if isApacheStarting == false %} -

    AIO password change

    - You can change your AIO password below:

    -
    - - + {% if isAnyRunning == true %} + {% if isApacheStarting != true %} + {% if is_mastercontainer_update_available == true %} +

    ⚠️ A mastercontainer update is available. Please click on the button below to stop your containers in order to update the mastercontainer.

    + {% if current_channel starts with 'latest' %} +

    You can find the changelog here

    + {% elseif current_channel starts with 'beta' %} +

    You can find the changelog here

    + {% elseif current_channel starts with 'develop' %} +

    You can find all changes here

    + {% endif %} + {% endif %} + - -
    - The new password needs to be at least 24 characters long. Allowed characters are the latin characters a-z, A-Z, 0-9 and spaces.

    - {% endif %} - {% endif %} - {% endif %} - {% if is_backup_container_running == false %} -

    Optional addons

    - In this section you can enable or disable optional addons.

    - {% if isAnyRunning == true %} - Please note: You can enable or disable them when your containers are stopped.

    - {% endif %} -
    - - - - {% if is_clamav_enabled == true %} -
    - {% else %} -
    - {% endif %} - {% if is_collabora_enabled == true %} -
    - {% else %} -
    - {% endif %} - {% if is_fulltextsearch_enabled == true %} -
    - {% else %} -
    - {% endif %} - {% if is_imaginary_enabled == true %} -
    - {% else %} -
    - {% endif %} - {% if is_talk_enabled == true %} -

    - {% else %} -

    - {% endif %} - {% if is_onlyoffice_enabled == true %} -
    - {% else %} - {#
    #} - {% endif %} - -
    - Minimal system requirements: When any optional addon is enabled, at least 2GB RAM, a dual-core CPU and 40GB system storage are required. When enabling ClamAV or Fulltextsearch, at least 3GB RAM are required. When enabling everything, at least 4GB RAM are required. Recommended are at least 1GB more RAM than the minimal requirement.

    - {% if isAnyRunning == true or is_x64_platform == false %} - - - {% endif %} - {% if isAnyRunning == true %} - - - - - {% endif %} - - {% if is_collabora_enabled == true and isAnyRunning == false and was_start_button_clicked == true %} -

    Collabora dictionaries

    - - {% if collabora_dictionaries == "" %} - In order to get the correct dictionaries in Collabora, you may configure the dictionaries below:

    -
    - - - - -
    - You need to make sure that the dictionaries that you enter are valid. An example is de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru.

    - {% else %} - The dictionaries for Collabora are currently set to {{ collabora_dictionaries }}. You can reset them again by clicking on the button below.

    -
    - - - - +
    {% endif %} - {% endif %} - -

    Timezone change

    - {% if isAnyRunning == true %} - {% if timezone != "" %} - The timezone for Nextcloud is currently set to {{ timezone }}.

    - {% endif %} - Please note: You can change the timezone when your containers are stopped.

    {% else %} - {% if timezone == "" %} - In order to get the correct time values for certain Nextcloud features, it makes sense to set the timezone for Nextcloud to the one that your users mainly use. Please note that this setting does not apply to the mastercontainer and any backup option.

    - You can configure the timezone for Nextcloud below:

    -
    - - - - -
    - You need to make sure that the timezone that you enter is valid. An example is Europe/Berlin. You can get valid values by looking at the 'TZ database name' column of this list: click here.

    + {% if isBackupOrRestoreRunning == true %} +

    Restore or Backup currently running. Cannot start the containers until Restore or Backup is complete.

    {% else %} - The timezone for Nextcloud is currently set to {{ timezone }}. You can reset the timezone again by clicking on the button below.

    -
    - - - - -
    + {% if was_start_button_clicked == false %} +

    Clicking on the button below will download all docker containers and start them. This can take a long time depending on your internet connection. Since the overall size is a few GB, this can take around 5-10 min or more. Please be patient!

    + {% endif %} + {% if is_mastercontainer_update_available == true %} +

    ⚠️ A mastercontainer update is available. Please click on the button below to update it.

    +
    + + + +
    + {% else %} + {% if was_start_button_clicked == false %} +
    + + + + {% if newMajorVersionString != '' %} +
    + {% endif %} + +
    + {% elseif has_update_available == false %} +
    + + + + +
    + {% else %} +
    + + + + {% if bypass_container_update == true %} + + {% endif %} + +
    + {% endif %} + {% endif %} {% endif %} {% endif %} + + {% if was_start_button_clicked == true %} + + {% if is_backup_section_enabled == false %} +

    Backup and restore

    +

    The backup section is disabled via environmental variable.

    + {% else %} + {% if is_backup_container_running == false and not hasBackupLocation and isApacheStarting != true %} +

    Backup and restore

    +

    Please enter the directory path below where backups will be created on the host system and submit it. It's best to choose a location on a separate drive and not on your root drive.

    +

    + To store backups remotely instead, fill in the + remote borg repo url and submit it. + You will be provided with an SSH public key for authorization at the remote afterwards. +

    +
    +
    +
    + + + +
    + {{ include('includes/backup-dirs.twig') }} + {% endif %} + {% endif %} + + {% if is_backup_section_enabled == true %} + + {% if hasBackupLocation %} + {% if is_backup_container_running == false %} +

    Backup and restore

    + {% if backup_exit_code > 0 %} +

    Last {{ borg_backup_mode }} failed! (Logs)

    + {% if borg_backup_mode == "check" %} +

    The backup check was not successful. This might indicate a corrupt archive (look at the logs). If that should be the case, you can try to fix it by following this documentation

    +
    + Reveal repair option +

    Below is the option to repair the integrity of your backup. Please note: Please only use this after you have read the documentation above! (It will run the command 'borg check --repair' for you.)

    +
    + + + +
    +
    + {% endif %} + {% if has_backup_run_once == false %} +

    The initial backup was not successful.

    + + {% if borg_remote_repo %} +

    + You may still need to authorize this pubkey on your borg remote:
    {{ borg_public_key }}
    + To try again, click Create backup. +

    + {% endif %} + +

    You may change the backup path again since the initial backup was not successful. After submitting the new value, you need to click on Create Backup to test the new value.

    +
    +
    +
    + + + +
    + {% endif %} + {% elseif backup_exit_code == 0 %} + {% if borg_backup_mode == "backup" %} +

    Last {{ borg_backup_mode }} successful on {{ last_backup_time }} UTC! (Logs)

    + {% else %} +

    Last {{ borg_backup_mode }} successful! (Logs)

    + {% endif %} + {% endif %} + {% endif %} + + {% if is_backup_container_running == false and isApacheStarting == false %} + {% if has_backup_run_once == true %} +
    + Click here to reveal all backup options (including an option for automatic updates) + {% endif %} +

    Backup information

    +

    This is your encryption password for backups: {{ borgbackup_password }}

    +

    Please save this password in a safe place. You won't be able to restore from backup if you lose this password!

    +

    All important data from your Nextcloud AIO instance such as the database, your files and the mastercontainer's configuration files, will be backed up.

    +

    The backup uses a tool called BorgBackup, a well-known server backup tool that efficiently backs up your files and encrypts them on the fly.

    +

    By using this tool, backups are incremental, differential, compressed and encrypted – so only the first backup will take a while. Further backups should be fast as only changes are taken into account.

    + {% if borg_remote_repo != '' %} +

    + Backups get created remotely at:
    + {{ borg_remote_repo }} + {% if has_backup_run_once == true %} +
    Your borg ssh public key is:
    {{ borg_public_key }} + {% endif %} +

    + {% else %} +

    Backups will be created in the following directory on the host: {{ borg_backup_host_location }}/borg

    + {% endif %} +

    Be aware that this solution does not backup files and folders that are mounted into Nextcloud using the external storage app, but you can add further Docker volumes and host paths that you want to back up after the initial backup is done.

    +

    For information about backup retention, see this.

    +

    Daily backups can be enabled after the initial backup is done. Enabling this also allows you to enable an option to update all containers, Nextcloud, and its apps automatically.

    +

    For further documentation and options on this backup solution refer to this section and below.

    + + {% if isApacheStarting != true %} +

    Backup creation

    +

    Clicking on the button below will create a backup.

    +
    + + + +
    + + {% if has_backup_run_once == true %} +

    Backup Viewer

    +

    There is now a community container that allows to access your backups in a web session. See this documentation.

    + +

    Backup check

    +

    Click on the button below to perform a backup integrity check. This is an option that verifies that your backup is intact. It shouldn't be needed in most situations.

    +
    + + + +
    + +

    Backup restore

    +

    Choose the backup that you want to restore and click on the button below to restore the selected backup. This will overwrite all your files with the chosen backup so you should consider creating a backup first. You can run an integrity check before restoring your files but this shouldn't be needed in most situations. Please note that this will not restore additionally chosen backup directories! The restore process should be pretty fast as rsync, which only transfers changed files, is used to restore the chosen backup.

    +
    + + + + +
    + +

    Update backup list

    +
    + Click here to reveal this option +

    If you use an external snapshot tool to restore the server that runs AIO, you might run into a problem that the above listed available backups are not up-to-date to restore your server from. You can click the button below to update this list.

    +
    + + + +
    +
    + +

    Daily backup and automatic updates

    + {% if daily_backup_time == "" %} +

    By entering a time below and submitting it, you can enable daily backups. It will create them at the entered time in 24h format. E.g. 04:00 will create backups at 4 am UTC and 16:00 at 4 pm UTC. When creating the backup, containers will be stopped and restarted after the backup is complete.

    +
    + + + +
    +
    + +
    + {% else %} +

    Daily backups will be created at {{ daily_backup_time }} UTC. A notification about the result of the backup will be sent.

    + {% if automatic_updates == true %} + Also your containers, the mastercontainer and, on Saturdays, your Nextcloud apps will be automatically updated. + {% endif %} +

    To change your backup time first disable Daily Backups, then enter your new backup time, and then re-enable them.

    +
    + + + + +
    + {% endif %} + +

    Back up additional directories and docker volumes of your host

    +

    Below you can enter directories and docker volumes of your host that will be backed up into the same borg backup archive. Make sure to press the submit button after changing anything.

    +
    + + + + +
    +

    Each line and entry needs to start with a slash or letter/digit. Only a-z, A-Z, ., 0-9, _, -, and / are allowed. If the entry begins with a letter/digit slashes are not supported. Two valid entries are /directory/on/the/host and my_custom_docker_volume. You need to make sure that all given directories exist or the backup container will fail to start!

    +

    Be sure to individually specify all storage that you want to back up as storage will not be mounted recursively. E.g. providing / as additional backup directory will only back up files and folders that are stored on the root partition and not on the EFI partition or any other. Excluded by the backup will be caches and a few other directories. If you want to back up the root partition you should make sure to stop all services before the backup so it can run correctly. For automating this see this documentation

    +

    Please note that the chosen directories/volumes will not be restored when you restore your instance, so this would need to be done manually.

    + {% if additional_backup_directories != "" %} +

    This option is currently set. You can disable it again by clearing the field and submitting your changes.

    + {% endif %} + {% endif %} + +

    Reset backup location

    +

    + If the configured backup host location {{ borg_backup_host_location }} + {% if borg_remote_repo %} + or the remote repo {{ borg_remote_repo }} + {% endif %} + is wrong or if you want to reset the backup location due to other reasons, you can do so by clicking on the button below. +

    +
    + + + + +
    + {% endif %} + {% if has_backup_run_once == true %} +
    + {% endif %} + {% endif %} + {% endif %} + {% endif %} + + {% if is_backup_container_running == false %} + {% if isApacheStarting == false %} +

    AIO passphrase change

    +
    + Click here to change your AIO passphrase +

    You can change your AIO passphrase below:

    +
    + + + + + +
    +

    The new passphrase needs to be at least 24 characters long. Allowed characters are the latin characters a-z, A-Z, 0-9 and spaces.

    +
    + {% endif %} + {% endif %} + {% endif %} + {% if is_backup_container_running == false %} + + {{ include('includes/optional-containers.twig') }} + +

    Timezone change

    + {% if isAnyRunning == true %} + {% if timezone != "" %} +

    The timezone for Nextcloud is currently set to {{ timezone }}.

    + {% endif %} +

    Please note: You can change the timezone when your containers are stopped.

    + {% else %} + {% if timezone == "" %} +

    To get the correct time values for certain Nextcloud features, set the timezone for Nextcloud to the one that your users mainly use. Please note that this setting does not apply to the mastercontainer and any backup option.

    +

    You can configure the timezone for Nextcloud below (Do not forget to submit the value!):

    +
    + + + + +
    +

    You need to make sure that the timezone that you enter is valid. An example is Europe/Berlin. You can get valid values by looking at the 'TZ identifier' column of this list: click here. The default is Etc/UTC if nothing is entered.

    + {% else %} +

    The timezone for Nextcloud is currently set to {{ timezone }}. You can change the timezone by clicking on the button below.

    +
    + + + + +
    + {% endif %} + {% endif %} + {{ include('includes/community-containers.twig') }} + {% endif %} {% endif %} {% endif %} + + {% if isApacheStarting == true or is_backup_container_running == true or isWatchtowerRunning == true or is_daily_backup_running == true %} + + {% else %} + {% endif %} - {% if isApacheStarting == true or is_backup_container_running == true or isWatchtowerRunning == true or is_daily_backup_running == true %} - - {% else %} - - {% endif %} + -
    -
    -
    +
    {% endblock %} diff --git a/php/templates/includes/aio-config.twig b/php/templates/includes/aio-config.twig new file mode 100644 index 00000000..fbb70230 --- /dev/null +++ b/php/templates/includes/aio-config.twig @@ -0,0 +1,44 @@ +
    + Click here to view the current AIO config and documentation links + {% if was_start_button_clicked == true %} +

    Nextcloud's config.php file is stored in the nextcloud_aio_nextcloud Docker volume and can be edited by following the config.php documentation.

    +

    You can run Nextcloud's usual occ commands by following the occ documentation.

    + {% endif %} + +

    + {% if nextcloud_datadir starts with '/' %} + Nextcloud's datadir is getting stored in the {{ nextcloud_datadir }} directory. + {% else %} + Nextcloud's datadir is getting stored in the {{ nextcloud_datadir }} Docker volume. + {% endif %} + See the NEXTCLOUD_DATADIR documentation on how to change this. +

    + +

    + {% if nextcloud_mount == '' %} + The Nextcloud container is confined and local external storage in Nextcloud is disabled. + {% else %} + The Nextcloud container is getting access to the {{ nextcloud_mount }} directory and local external storage in Nextcloud is enabled. + {% endif %} + See the NEXTCLOUD_MOUNT documentation on how to change this.

    + +

    Nextcloud has an upload limit of {{ nextcloud_upload_limit }} configured (for public link uploads. Bigger uploads are always possible when users are logged in). See the NEXTCLOUD_UPLOAD_LIMIT documentation on how to change this.

    + +

    For Nextcloud, a memory limit of {{ nextcloud_memory_limit }} per PHP process is configured. See the NEXTCLOUD_MEMORY_LIMIT documentation on how to change this.

    + +

    Nextcloud has a timeout of {{ nextcloud_max_time }} seconds configured (important for big file uploads). See the NEXTCLOUD_MAX_TIME documentation on how to change this.

    + +

    + {% if is_dri_device_enabled == true and is_nvidia_gpu_enabled == true %} + Hardware acceleration is enabled with the /dev/dri device and the Nvidia runtime. + {% elseif is_dri_device_enabled == true %} + Hardware acceleration is enabled with the /dev/dri device. + {% elseif is_nvidia_gpu_enabled == true %} + Hardware acceleration is enabled with the Nvidia runtime. + {% else %} + Hardware acceleration is not enabled. It's recommended to enable hardware transcoding for better performance. + {% endif %} + See the hardware acceleration documentation on how to change this.

    + +

    For further documentation on AIO, refer to this page. You can use the browser search [CTRL]+[F] to search through the documentation. Additional documentation can be found here.

    +
    diff --git a/php/templates/includes/aio-version.twig b/php/templates/includes/aio-version.twig new file mode 100644 index 00000000..1b62f917 --- /dev/null +++ b/php/templates/includes/aio-version.twig @@ -0,0 +1 @@ +12.6.1 diff --git a/php/templates/includes/backup-dirs.twig b/php/templates/includes/backup-dirs.twig new file mode 100644 index 00000000..390bf69c --- /dev/null +++ b/php/templates/includes/backup-dirs.twig @@ -0,0 +1,6 @@ +

    The folder path that you enter must start with / and must not end with /.

    +

    An example for Linux is /mnt/backup.

    +

    On Synology it could be /volume1/docker/nextcloud/backup.

    +

    For macOS it may be /var/backup.

    +

    On Windows it might be /run/desktop/mnt/host/c/backup. (This path is equivalent to 'C:\backup' on your Windows host so you need to translate the path accordingly. Hint: the path that you enter needs to start with '/run/desktop/mnt/host/'. Append to that the exact location on your windows host, e.g. 'c/backup' which is equivalent to 'C:\backup'.) ⚠️ Please note: This does not work with external drives like USB or network drives and only with internal drives like SATA or NVME drives.

    +

    Another option is to enter a specific volume name here: nextcloud_aio_backupdir. This volume needs to be created beforehand manually by you in order to be able to use it. See this documentation for an example.

    diff --git a/php/templates/includes/community-containers.twig b/php/templates/includes/community-containers.twig new file mode 100644 index 00000000..66cceb2b --- /dev/null +++ b/php/templates/includes/community-containers.twig @@ -0,0 +1,42 @@ +

    Community Containers

    +

    In this section you can enable or disable optional Community Containers that are not included by default in the main installation. These containers are provided by the community and can be useful for various purposes and are automatically integrated in AIOs backup solution and update mechanisms.

    +

    ⚠️ Caution: Community Containers are maintained by the community and not officially by Nextcloud. Some containers may not be compatible with your system, may not work as expected or may discontinue. Use them at your own risk. Please read the documentation for each container first before adding any as some are also incompatible between each other! Never add all of them at the same time!

    +{% if isAnyRunning == true %} +

    Please note: You can enable or disable the options below only when your containers are stopped.

    +{% else %} +

    Please note: Make sure to save your changes by clicking Save changes below the list of Community Containers. The changes will not be auto-saved.

    +{% endif %} +
    + Show/Hide available Community Containers +
    + + + + {% for cc in community_containers %} +

    + + +

    + {% endfor %} + + +
    +
    diff --git a/php/templates/includes/optional-containers.twig b/php/templates/includes/optional-containers.twig new file mode 100644 index 00000000..eabcb139 --- /dev/null +++ b/php/templates/includes/optional-containers.twig @@ -0,0 +1,273 @@ +

    Optional containers

    +

    In this section you can enable or disable optional containers.

    +{% if isAnyRunning == true %} +

    Please note: You can enable or disable the options below only when your containers are stopped.

    +{% else %} +

    Please note: Make sure to save your changes by clicking Save changes below the list of optional containers. The changes will not be auto-saved.

    +{% endif %} +
    + + + +

    Office Suite

    + {% if isAnyRunning == false %} +

    Choose your preferred office suite. Only one can be enabled at a time.

    + {% endif %} +
    + + + + + + +
    + {% if isAnyRunning == false %} +
    + + +
    + {% endif %} + +

    Additional Optional Containers

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + +

    + + +

    +

    + + +

    + +
    +

    Minimal system requirements: When any optional container is enabled, at least 2GB RAM, a dual-core CPU and 40GB system storage are required. When enabling ClamAV, Nextcloud Talk Recording-server or Fulltextsearch, at least 3GB RAM are required. For Talk Recording-server additional 2 vCPUs are required. When enabling everything, at least 5GB RAM and a quad-core CPU are required. Recommended are at least 1GB more RAM than the minimal requirement. For further advice and recommendations see this documentation

    +{% if isAnyRunning == true %} + + + + + + + + + +{% endif %} + +{% if is_collabora_enabled == true and isAnyRunning == false and was_start_button_clicked == true %} +

    Nextcloud Office dictionaries

    + + {% if collabora_dictionaries == "" %} +

    In order to get the correct dictionaries in Nextcloud Office, you may configure the dictionaries below:

    +
    + + + + +
    +

    You need to make sure that the dictionaries that you enter are valid. An example is de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru.

    + {% else %} +

    The dictionaries for Nextcloud Office are currently set to {{ collabora_dictionaries }}. You can reset them again by clicking on the button below.

    +
    + + + + +
    + {% endif %} + +

    Additional Nextcloud Office options

    + + {% if collabora_additional_options == "" %} +

    You can configure additional options for Nextcloud Office below.

    +

    (This can be used for configuring the net.content_security_policy and more. Make sure to submit the value!)

    +
    + + + + +
    +

    You need to make sure that the options that you enter are valid. An example is --o:net.content_security_policy=frame-ancestors *.example.com:*;.

    + {% else %} +

    The additioinal options for Nextcloud Office are currently set to {{ collabora_additional_options }}. You can reset them again by clicking on the button below.

    +
    + + + + +
    + {% endif %} +{% endif %} diff --git a/php/templates/layout.twig b/php/templates/layout.twig index 08b5af0b..79c615d9 100644 --- a/php/templates/layout.twig +++ b/php/templates/layout.twig @@ -1,13 +1,21 @@ AIO - - + + - + +
    {% block body %}{% endblock %} +
    +
    +
    +
    + diff --git a/php/templates/login.twig b/php/templates/login.twig index 38229a7c..1c5420c2 100644 --- a/php/templates/login.twig +++ b/php/templates/login.twig @@ -1,23 +1,25 @@ -{% extends "layout.twig" %} - -{% block body %} - -{% endblock %} - +{% extends "layout.twig" %} + +{% block body %} + + +{% endblock %} diff --git a/php/templates/setup.twig b/php/templates/setup.twig index 142a01d5..7cc9227a 100644 --- a/php/templates/setup.twig +++ b/php/templates/setup.twig @@ -1,14 +1,16 @@ {% extends "layout.twig" %} {% block body %} -