From df91de5cb838c269e426c85e2aa1220e8d8bcf7f Mon Sep 17 00:00:00 2001 From: Romeo Cortez Date: Wed, 30 Sep 2026 11:46:12 -0500 Subject: [PATCH 1/5] ci(deployment): validate AIO compose configuration Assisted-by: GitHubCopilot:Auto Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment-compose.yml | 36 ++++++++++++++++++++++++ deployment/README.md | 14 +++++++++ deployment/compose.yaml | 20 +++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 .github/workflows/deployment-compose.yml create mode 100644 deployment/README.md create mode 100644 deployment/compose.yaml diff --git a/.github/workflows/deployment-compose.yml b/.github/workflows/deployment-compose.yml new file mode 100644 index 0000000000000..e498dfc009cc7 --- /dev/null +++ b/.github/workflows/deployment-compose.yml @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Deployment configuration + +on: + pull_request: + paths: + - 'deployment/compose.yaml' + - '.github/workflows/deployment-compose.yml' + push: + branches: + - master + paths: + - 'deployment/compose.yaml' + - '.github/workflows/deployment-compose.yml' + +permissions: + contents: read + +concurrency: + group: deployment-compose-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Validate Docker Compose configuration + run: docker compose --file deployment/compose.yaml config --quiet diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 0000000000000..95ac0e919f220 --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,14 @@ + + +# Nextcloud AIO deployment + +This Compose file starts the official [Nextcloud All-in-One (AIO) mastercontainer](https://github.com/nextcloud/all-in-one). AIO creates and manages the Nextcloud and supporting containers, including its database and cache. The Compose file deliberately does not define or scale those child containers. + +The mastercontainer setup interface is bound to `127.0.0.1:8080`; access it locally or through an SSH tunnel. The AIO Apache endpoint is configured on `127.0.0.1:11000` for a host-based reverse proxy. Configure and secure that proxy separately before exposing the service to users. The Docker socket mount gives AIO control over the host's Docker daemon, so run this only on a trusted host. + +This AIO configuration is not a horizontally scaled deployment: AIO does not provide the separate load balancer, replicated Nextcloud app servers, or independently managed PostgreSQL and Redis services shown in the architecture diagram. Deployments requiring those components must use a separately designed architecture rather than scaling AIO-managed containers. + +The `Deployment configuration` workflow validates the Compose file on pull requests and pushes to `master`; it does not deploy or start containers. diff --git a/deployment/compose.yaml b/deployment/compose.yaml new file mode 100644 index 0000000000000..4b5e5921f2865 --- /dev/null +++ b/deployment/compose.yaml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +services: + nextcloud-aio-mastercontainer: + image: ghcr.io/nextcloud-releases/all-in-one:latest + init: true + restart: always + container_name: nextcloud-aio-mastercontainer + volumes: + - nextcloud_aio_mastercontainer:/mnt/docker-aio-config + - /var/run/docker.sock:/var/run/docker.sock:ro + ports: + - "127.0.0.1:8080:8080" + environment: + APACHE_PORT: "11000" + APACHE_IP_BINDING: 127.0.0.1 + +volumes: + nextcloud_aio_mastercontainer: + name: nextcloud_aio_mastercontainer From 749c52af1386730fd32c64a9060d704813e97074 Mon Sep 17 00:00:00 2001 From: Romeo Cortez Date: Wed, 30 Sep 2026 14:49:58 -0500 Subject: [PATCH 2/5] revert(deployment): remove initial AIO deployment setup Remove the deployment example and configuration validation workflow to restart the CI/CD design from a clean branch state. This reverts df91de5cb83. Assisted-by: GitHubCopilot:Auto Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment-compose.yml | 36 ------------------------ deployment/README.md | 14 --------- deployment/compose.yaml | 20 ------------- 3 files changed, 70 deletions(-) delete mode 100644 .github/workflows/deployment-compose.yml delete mode 100644 deployment/README.md delete mode 100644 deployment/compose.yaml diff --git a/.github/workflows/deployment-compose.yml b/.github/workflows/deployment-compose.yml deleted file mode 100644 index e498dfc009cc7..0000000000000 --- a/.github/workflows/deployment-compose.yml +++ /dev/null @@ -1,36 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Deployment configuration - -on: - pull_request: - paths: - - 'deployment/compose.yaml' - - '.github/workflows/deployment-compose.yml' - push: - branches: - - master - paths: - - 'deployment/compose.yaml' - - '.github/workflows/deployment-compose.yml' - -permissions: - contents: read - -concurrency: - group: deployment-compose-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - validate: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Validate Docker Compose configuration - run: docker compose --file deployment/compose.yaml config --quiet diff --git a/deployment/README.md b/deployment/README.md deleted file mode 100644 index 95ac0e919f220..0000000000000 --- a/deployment/README.md +++ /dev/null @@ -1,14 +0,0 @@ - - -# Nextcloud AIO deployment - -This Compose file starts the official [Nextcloud All-in-One (AIO) mastercontainer](https://github.com/nextcloud/all-in-one). AIO creates and manages the Nextcloud and supporting containers, including its database and cache. The Compose file deliberately does not define or scale those child containers. - -The mastercontainer setup interface is bound to `127.0.0.1:8080`; access it locally or through an SSH tunnel. The AIO Apache endpoint is configured on `127.0.0.1:11000` for a host-based reverse proxy. Configure and secure that proxy separately before exposing the service to users. The Docker socket mount gives AIO control over the host's Docker daemon, so run this only on a trusted host. - -This AIO configuration is not a horizontally scaled deployment: AIO does not provide the separate load balancer, replicated Nextcloud app servers, or independently managed PostgreSQL and Redis services shown in the architecture diagram. Deployments requiring those components must use a separately designed architecture rather than scaling AIO-managed containers. - -The `Deployment configuration` workflow validates the Compose file on pull requests and pushes to `master`; it does not deploy or start containers. diff --git a/deployment/compose.yaml b/deployment/compose.yaml deleted file mode 100644 index 4b5e5921f2865..0000000000000 --- a/deployment/compose.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -services: - nextcloud-aio-mastercontainer: - image: ghcr.io/nextcloud-releases/all-in-one:latest - init: true - restart: always - container_name: nextcloud-aio-mastercontainer - volumes: - - nextcloud_aio_mastercontainer:/mnt/docker-aio-config - - /var/run/docker.sock:/var/run/docker.sock:ro - ports: - - "127.0.0.1:8080:8080" - environment: - APACHE_PORT: "11000" - APACHE_IP_BINDING: 127.0.0.1 - -volumes: - nextcloud_aio_mastercontainer: - name: nextcloud_aio_mastercontainer From 449725eb79d68dd46527efb28438dcce3a4188a7 Mon Sep 17 00:00:00 2001 From: Romeo Cortez Date: Wed, 30 Sep 2026 16:17:09 -0500 Subject: [PATCH 3/5] ci(deployment): smoke test scalable demo stack Add a Compose demo of the accepted load-balanced Nextcloud topology with shared app state, PostgreSQL, and Redis. Exercise service startup, load-balanced status, and backup artifacts in GitHub Actions; document how this demo relates to existing application CI and its production limitations. Assisted-by: GitHubCopilot:Auto Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/CI-CD.md | 49 ++++++++++ .github/workflows/deployment-architecture.yml | 85 +++++++++++++++++ .gitignore | 3 + deployment/.env.example | 7 ++ deployment/README.md | 46 ++++++++++ deployment/backup.sh | 53 +++++++++++ deployment/compose.yaml | 91 +++++++++++++++++++ deployment/haproxy.cfg | 33 +++++++ 8 files changed, 367 insertions(+) create mode 100644 .github/CI-CD.md create mode 100644 .github/workflows/deployment-architecture.yml create mode 100644 deployment/.env.example create mode 100644 deployment/README.md create mode 100644 deployment/backup.sh create mode 100644 deployment/compose.yaml create mode 100644 deployment/haproxy.cfg diff --git a/.github/CI-CD.md b/.github/CI-CD.md new file mode 100644 index 0000000000000..b30397f343107 --- /dev/null +++ b/.github/CI-CD.md @@ -0,0 +1,49 @@ + + +# CI/CD and the deployment architecture + +## Scope + +This repository is the Nextcloud Server application. Its existing GitHub Actions workflows provide continuous integration for server code. A separate Compose smoke-test workflow starts a local demonstration of the group deployment topology and verifies service startup and backup artifact integrity. Neither workflow deploys to a live service or uses production credentials. + +The deployment design has a load balancer and multiple Nextcloud app servers, with PostgreSQL and Redis remaining single instances. Backups are explicit, and optional/community apps remain extensible. `.github/workflows/deployment-architecture.yml` tests this topology using the published Nextcloud Apache image; it does not build an image from this server source checkout. + +## Pipeline + +The normal entry point is a pull request. Jobs use path filters where applicable to avoid running unrelated test suites. Scheduled runs provide additional coverage for selected test suites. + +1. **PHP checks:** `.github/workflows/lint-php.yml` runs PHP linting on PHP 8.3 and 8.5. `.github/workflows/static-code-analysis.yml` runs Psalm and taint analysis for applicable changes. +2. **Server unit tests:** `.github/workflows/phpunit.yml` fans out to database and storage configurations, including PostgreSQL, MySQL/MariaDB, SQLite, and other supported service combinations. Its summary job reports failure if a required test job fails. +3. **Frontend unit tests:** `.github/workflows/node-test.yml` installs the repository's Node dependencies and runs `npm run test:coverage` when frontend source paths change. +4. **Feature integration tests:** `.github/workflows/integration-sqlite.yml` runs selected Behat suites with SQLite and supporting services such as Redis and OpenLDAP. +5. **Browser end-to-end tests:** `.github/workflows/playwright.yml` builds the frontend and runs sharded Playwright suites when its label or changed-test conditions allow them. It is not run for every ordinary pull request. +6. **Deployment architecture smoke test:** `.github/workflows/deployment-architecture.yml` validates Compose, starts PostgreSQL and Redis, bootstraps one app instance, scales the app service to three instances, starts HAProxy, checks Nextcloud's status endpoint through the load balancer, creates database/data/config/custom-app backups with maintenance mode enabled, restores the SQL dump into a scratch database, extracts the file archives, and checks checksums. The workflow tears down its ephemeral stack and volumes even on failure. +7. **Compliance:** `.github/workflows/reuse.yml` checks license and copyright metadata on pull requests. +8. **Merge gate:** Review the required checks on the pull request. Fix any failed job before merging; skipped path-filtered jobs mean their covered files were not changed. + +To test a deployment change, push a branch or open a pull request that changes `deployment/**` or the workflow. The architecture smoke test also supports manual dispatch after the workflow is present on the default branch. Existing application tests remain responsible for validating server source changes. This is not a production deployment pipeline. + +## Architecture coverage and gaps + +| Architecture concern | What CI can check | What this CI does not prove | +| --- | --- | --- | +| Multiple app servers | The deployment smoke test runs three app containers behind HAProxy and checks the public status endpoint. App containers share the web-root volume; Redis is common to all instances and the official image configures Redis-backed PHP sessions. | Production load, balancing distribution, app-node failover, independent-host shared storage, TLS termination, and high availability. | +| PostgreSQL | PHPUnit includes a PostgreSQL test workflow; the deployment smoke test uses one PostgreSQL service. | Production database replication, capacity, recovery, or failover. | +| Redis | Existing test jobs exercise Redis-backed services; the smoke test uses one Redis instance. | Production Redis availability, persistence guarantees, clustering, capacity, or failover. | +| Explicit backups | `deployment/backup.sh` enables maintenance mode and writes PostgreSQL, data, config, and custom-app archives with checksums. CI restores the database dump into a scratch database and extracts the file archives. | Off-host backup retention, encryption/key management, scheduled execution, and a full service recovery drill. Keep backups on protected storage and test restores before relying on them. | +| Optional/community apps | Existing app and integration suites test the app code and selected integrations represented in those suites. | Compatibility of every optional/community app or its production container image. | +| HTTPS reverse proxy and load balancer | Not represented by these application test workflows. | TLS termination, proxy headers, network policy, and end-to-end traffic routing. | + +This is a local/demo topology, not production-ready infrastructure. PostgreSQL and Redis remain single-instance failure points. The Compose endpoint uses HTTP bound to loopback, not public TLS. Backups are written to the local host and must be copied to protected off-host storage. A production rollout still needs a target environment, TLS, protected secrets, durable shared storage, backup retention and restore drills, monitoring, and operational review. + +## AI-assisted design record + +- **Initial user request:** “add or fix the current ci/cd to this new improved architecture” +- **Architecture review prompt supplied by the group:** “Review Your System (5 min) Work with your group Review your Module 1 architecture diagram. (You are always welcome to modify it) Identify what your pipeline needs to build and test, and eventually deploy. Identify anything in your architecture that the CI/CD workflow needs to account for. Use AI to Build the pipeline (15 min) You may ask AI to: Generate the pipeline configuration. Explain the configuration. Modify the workflow when your system requires something different. Document every step in details (AI prompts, what AI generated, and any decisions or changes your group made.)” +- **Group decisions shown in the architecture review:** Accept app-server scalability using a load balancer and multiple app servers; make backups explicit; retain the optional/community-app split. Modify the single-point-of-failure recommendation so app servers scale while PostgreSQL and Redis remain single instances. Reject the admin deployment-flow view in favor of a component architecture. +- **Scope clarifications:** First, “Build CI for this repo’s app tests and architecture documentation only (Recommended).” Later, “now make it work with our improved and accepted reccomendation and architecture”; the selected target was “Docker Compose for a class/demo deployment (Recommended).” +- **Generated outcome:** Retained and documented the existing application tests, then added Compose services for HAProxy, three scalable Nextcloud app instances, one PostgreSQL instance, one Redis instance, and persistent named volumes. Added a maintenance-mode backup script for the database, data, config, and custom apps. +- **Implementation decision:** CI starts and smoke-tests this local topology and validates backup artifacts; it does not deploy to a live host. PostgreSQL and Redis remain single instances per the group's modified recommendation. Production TLS, HA, off-host backup retention, and restore drills remain follow-up work. diff --git a/.github/workflows/deployment-architecture.yml b/.github/workflows/deployment-architecture.yml new file mode 100644 index 0000000000000..82605ef63f0e1 --- /dev/null +++ b/.github/workflows/deployment-architecture.yml @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Deployment architecture + +on: + pull_request: + paths: + - 'deployment/**' + - '.github/workflows/deployment-architecture.yml' + push: + paths: + - 'deployment/**' + - '.github/workflows/deployment-architecture.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: deployment-architecture-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + compose-smoke-test: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + COMPOSE_PROJECT_NAME: nextcloud-architecture + POSTGRES_PASSWORD: ci-only-postgres-password + NEXTCLOUD_ADMIN_USER: admin + NEXTCLOUD_ADMIN_PASSWORD: ci-only-nextcloud-password + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Validate Compose configuration + run: docker compose --file deployment/compose.yaml config --quiet + + - name: Start database, cache, and first app server + run: docker compose --file deployment/compose.yaml up --detach --wait db redis app + + - name: Scale app servers and start load balancer + run: | + docker compose --file deployment/compose.yaml up --detach --wait --scale app=3 app + docker compose --file deployment/compose.yaml up --detach --wait load-balancer + + - name: Verify load-balanced Nextcloud and three app servers + run: | + docker compose --file deployment/compose.yaml ps --format json \ + | jq --slurp --exit-status '[.[] | select(.Service == "app" and .State == "running")] | length == 3' + curl --fail --silent --show-error --retry 20 --retry-all-errors --retry-delay 2 \ + http://127.0.0.1:8080/status.php | jq --exit-status '.installed == true' + + - name: Create and verify database and file backups + run: | + backup_dir="$RUNNER_TEMP/nextcloud-backups" + bash deployment/backup.sh "$backup_dir" + database_dump="$(find "$backup_dir" -name 'database-*.dump' -print -quit)" + data_archive="$(find "$backup_dir" -name 'data-*.tar.gz' -print -quit)" + config_archive="$(find "$backup_dir" -name 'config-*.tar.gz' -print -quit)" + apps_archive="$(find "$backup_dir" -name 'custom-apps-*.tar.gz' -print -quit)" + test -s "$database_dump" + test -s "$data_archive" + test -s "$config_archive" + test -s "$apps_archive" + docker compose --file deployment/compose.yaml exec -T db createdb -U nextcloud nextcloud_restore_check + docker compose --file deployment/compose.yaml exec -T db \ + pg_restore --exit-on-error -U nextcloud -d nextcloud_restore_check < "$database_dump" + docker compose --file deployment/compose.yaml exec -T db \ + psql --no-psqlrc -U nextcloud -d nextcloud_restore_check -tAc \ + "SELECT to_regclass('public.oc_appconfig') IS NOT NULL" | grep --fixed-strings --line-regexp t + mkdir -p "$backup_dir/restored-data" "$backup_dir/restored-config" "$backup_dir/restored-apps" + tar --extract --gzip --file "$data_archive" --directory "$backup_dir/restored-data" + tar --extract --gzip --file "$config_archive" --directory "$backup_dir/restored-config" + tar --extract --gzip --file "$apps_archive" --directory "$backup_dir/restored-apps" + test -f "$backup_dir/restored-config/config.php" + shasum --check "$backup_dir"/SHA256SUMS-* + + - name: Stop test stack + if: always() + run: docker compose --file deployment/compose.yaml down --volumes --remove-orphans diff --git a/.gitignore b/.gitignore index 46a6155c8485c..bfb5eb56e1c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -154,6 +154,9 @@ Vagrantfile /tests/coverage* /tests/css /tests/karma-coverage +/deployment/backups/ +/deployment/.env +/.env /tests/autoconfig* /tests/autotest* /tests/data/lorem-copy.txt diff --git a/deployment/.env.example b/deployment/.env.example new file mode 100644 index 0000000000000..cab491b033202 --- /dev/null +++ b/deployment/.env.example @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Local demonstration credentials only. Replace these before any non-demo use. +POSTGRES_PASSWORD=local-demo-change-me +NEXTCLOUD_ADMIN_USER=admin +NEXTCLOUD_ADMIN_PASSWORD=local-demo-change-me diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 0000000000000..8b25451c4b2e9 --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,46 @@ + + +# Scalable Nextcloud demo stack + +This Compose stack demonstrates the group's accepted topology: HAProxy routes requests to multiple Nextcloud app containers, which share the Nextcloud web root and use one PostgreSQL database and one Redis cache/session store. PostgreSQL and Nextcloud state use persistent named volumes; Redis is intentionally ephemeral. + +This is a class/demo stack, not a production deployment. It binds plain HTTP to `127.0.0.1:8080`; it has no TLS termination, high availability for PostgreSQL or Redis, off-host backups, monitoring, or restore automation. Do not expose it publicly or use the example credentials outside a disposable local environment. Pin and review image versions and secrets before any real deployment. + +## Run locally + +From the repository root: + +```sh +cp deployment/.env.example .env +docker compose --file deployment/compose.yaml up --detach --wait db redis app +docker compose --file deployment/compose.yaml up --detach --wait --scale app=3 app +docker compose --file deployment/compose.yaml up --detach --wait load-balancer +curl --fail http://127.0.0.1:8080/status.php +``` + +Start the first app container before scaling it so initial setup completes and becomes healthy before additional instances join. The status endpoint should report `"installed": true` before treating the stack as ready. + +The HAProxy frontend discovers up to three app containers through Docker's internal DNS. To add more instances, update the HAProxy `server-template` slot count and the scale command together. + +## Create a local backup + +With the stack running, execute: + +```sh +bash deployment/backup.sh +``` + +The script enables Nextcloud maintenance mode, writes a custom-format PostgreSQL dump plus compressed data, config, and custom-app archives under `deployment/backups/`, writes SHA-256 checksums, and turns maintenance mode off. The backup directory is ignored by Git. Config backups contain credentials; protect them. Copy backups to separate protected storage and rehearse restores before relying on them. + +Stop the demo stack with `docker compose --file deployment/compose.yaml down`. Named volumes are retained. To remove the demo data as well, use `docker compose --file deployment/compose.yaml down --volumes`. + +## CI + +The `Deployment architecture` GitHub Actions workflow runs on pull requests and pushes affecting `deployment/**` or the workflow itself. It validates Compose, starts the services, scales Nextcloud to three app containers, checks the app through HAProxy, creates a backup, and verifies the database dump, tar archives, and checksums. It always tears down the ephemeral CI stack and volumes. + +The workflow consumes the published `nextcloud:apache` image; it tests the deployment topology, not a container image built from this repository's source. Existing PHPUnit, Node, integration, lint, and static-analysis workflows continue to validate application changes separately. The topology smoke test does not prove production capacity, node failover, TLS, or disaster recovery. + +See [CI/CD and the deployment architecture](../.github/CI-CD.md) for the complete pipeline map, coverage limits, and design decisions. diff --git a/deployment/backup.sh b/deployment/backup.sh new file mode 100644 index 0000000000000..50aa7b0bf4fe8 --- /dev/null +++ b/deployment/backup.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +set -euo pipefail +umask 077 + +backup_dir="${1:-deployment/backups}" +timestamp="$(date -u +%Y%m%dT%H%M%SZ)-$$" +maintenance_enabled=0 +backup_complete=0 + +mkdir -p "$backup_dir" + +cleanup() { + status=$? + if [ "$backup_complete" -eq 0 ]; then + rm -f -- "$backup_dir/database-$timestamp.dump" \ + "$backup_dir/data-$timestamp.tar.gz" \ + "$backup_dir/config-$timestamp.tar.gz" \ + "$backup_dir/custom-apps-$timestamp.tar.gz" \ + "$backup_dir/SHA256SUMS-$timestamp" + fi + if [ "$maintenance_enabled" -eq 1 ]; then + if ! docker compose --file deployment/compose.yaml exec --index=1 -T app php occ maintenance:mode --off; then + echo "Failed to disable Nextcloud maintenance mode" >&2 + status=1 + fi + fi + exit "$status" +} + +trap cleanup EXIT +trap 'exit 1' HUP INT TERM + +docker compose --file deployment/compose.yaml exec --index=1 -T app php occ maintenance:mode --on +maintenance_enabled=1 + +docker compose --file deployment/compose.yaml exec -T db sh -ec \ + 'pg_dump --username="$POSTGRES_USER" --dbname="$POSTGRES_DB" --format=custom' \ + > "$backup_dir/database-$timestamp.dump" +docker compose --file deployment/compose.yaml exec --index=1 -T app \ + tar -czf - -C /var/www/html/data . > "$backup_dir/data-$timestamp.tar.gz" +docker compose --file deployment/compose.yaml exec --index=1 -T app \ + tar -czf - -C /var/www/html/config . > "$backup_dir/config-$timestamp.tar.gz" +docker compose --file deployment/compose.yaml exec --index=1 -T app \ + tar -czf - -C /var/www/html/custom_apps . > "$backup_dir/custom-apps-$timestamp.tar.gz" + +shasum -a 256 "$backup_dir"/database-"$timestamp".dump \ + "$backup_dir"/data-"$timestamp".tar.gz \ + "$backup_dir"/config-"$timestamp".tar.gz \ + "$backup_dir"/custom-apps-"$timestamp".tar.gz > "$backup_dir/SHA256SUMS-$timestamp" +backup_complete=1 diff --git a/deployment/compose.yaml b/deployment/compose.yaml new file mode 100644 index 0000000000000..70646f5849e3f --- /dev/null +++ b/deployment/compose.yaml @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +services: + db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: nextcloud + POSTGRES_USER: nextcloud + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] + interval: 5s + timeout: 5s + retries: 12 + networks: + - backend + + redis: + image: redis:7-alpine + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 12 + networks: + - backend + + app: + image: nextcloud:apache + restart: unless-stopped + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + environment: + POSTGRES_HOST: db + POSTGRES_DB: nextcloud + POSTGRES_USER: nextcloud + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} + REDIS_HOST: redis + NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USER:-admin} + NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD:?Set NEXTCLOUD_ADMIN_PASSWORD in .env} + NEXTCLOUD_TRUSTED_DOMAINS: localhost localhost:8080 127.0.0.1 127.0.0.1:8080 + TRUSTED_PROXIES: 172.30.0.0/24 + OVERWRITEHOST: localhost:8080 + OVERWRITEPROTOCOL: http + APACHE_DISABLE_REWRITE_IP: "1" + volumes: + - nextcloud_html:/var/www/html + healthcheck: + test: + - CMD-SHELL + - >- + php -r '$$status = json_decode(file_get_contents("http://127.0.0.1/status.php"), true); + exit(($$status["installed"] ?? false) ? 0 : 1);' + interval: 10s + timeout: 5s + retries: 30 + start_period: 30s + networks: + - frontend + - backend + + load-balancer: + image: haproxy:3.2-alpine + restart: unless-stopped + depends_on: + app: + condition: service_healthy + ports: + - "127.0.0.1:8080:80" + volumes: + - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro + networks: + - frontend + +volumes: + postgres_data: + nextcloud_html: + +networks: + frontend: + ipam: + config: + - subnet: 172.30.0.0/24 + backend: diff --git a/deployment/haproxy.cfg b/deployment/haproxy.cfg new file mode 100644 index 0000000000000..cd0bfa51f8d15 --- /dev/null +++ b/deployment/haproxy.cfg @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +global + log stdout format raw local0 + maxconn 1024 + +defaults + log global + mode http + option httplog + timeout connect 5s + timeout client 30s + timeout server 30s + +resolvers docker + nameserver dns 127.0.0.11:53 + resolve_retries 3 + timeout resolve 1s + timeout retry 1s + hold valid 10s + +frontend web + bind :80 + http-request set-header X-Forwarded-Proto http + http-request set-header X-Forwarded-Host %[req.hdr(Host)] + default_backend nextcloud + +backend nextcloud + balance roundrobin + option forwardfor + http-check send meth GET uri /status.php ver HTTP/1.1 hdr Host localhost:8080 + http-check expect status 200 + server-template app 3 app:80 check resolvers docker init-addr libc,none From b24f41923f88650077fe268ee42d04b14fdd0ba9 Mon Sep 17 00:00:00 2001 From: Romeo Cortez Date: Wed, 30 Sep 2026 16:26:15 -0500 Subject: [PATCH 4/5] ci(deployment): fix running app count assertion Use Compose's running-container filter instead of relying on formatted JSON fields to count the scaled app instances. Assisted-by: GitHubCopilot:Auto Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment-architecture.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deployment-architecture.yml b/.github/workflows/deployment-architecture.yml index 82605ef63f0e1..196579ff2718e 100644 --- a/.github/workflows/deployment-architecture.yml +++ b/.github/workflows/deployment-architecture.yml @@ -50,8 +50,9 @@ jobs: - name: Verify load-balanced Nextcloud and three app servers run: | - docker compose --file deployment/compose.yaml ps --format json \ - | jq --slurp --exit-status '[.[] | select(.Service == "app" and .State == "running")] | length == 3' + running_apps="$(docker compose --file deployment/compose.yaml ps --status running --quiet app | wc -l | tr -d ' ')" + echo "Running Nextcloud app containers: $running_apps" + test "$running_apps" -eq 3 curl --fail --silent --show-error --retry 20 --retry-all-errors --retry-delay 2 \ http://127.0.0.1:8080/status.php | jq --exit-status '.installed == true' From 0013bcb28c7231b8470f0cc342badff448b14cac Mon Sep 17 00:00:00 2001 From: Romeo Cortez Date: Wed, 30 Sep 2026 16:28:59 -0500 Subject: [PATCH 5/5] ci(deployment): preserve scaled app replicas Start the load balancer in the same Compose invocation that scales the app service. Later Compose up commands reconcile service replicas and would otherwise reduce the app tier back to one container. Assisted-by: GitHubCopilot:Auto Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deployment-architecture.yml | 4 +--- deployment/README.md | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deployment-architecture.yml b/.github/workflows/deployment-architecture.yml index 196579ff2718e..20c1e4dd83025 100644 --- a/.github/workflows/deployment-architecture.yml +++ b/.github/workflows/deployment-architecture.yml @@ -44,9 +44,7 @@ jobs: run: docker compose --file deployment/compose.yaml up --detach --wait db redis app - name: Scale app servers and start load balancer - run: | - docker compose --file deployment/compose.yaml up --detach --wait --scale app=3 app - docker compose --file deployment/compose.yaml up --detach --wait load-balancer + run: docker compose --file deployment/compose.yaml up --detach --wait --scale app=3 load-balancer - name: Verify load-balanced Nextcloud and three app servers run: | diff --git a/deployment/README.md b/deployment/README.md index 8b25451c4b2e9..89a747ce64478 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -16,12 +16,11 @@ From the repository root: ```sh cp deployment/.env.example .env docker compose --file deployment/compose.yaml up --detach --wait db redis app -docker compose --file deployment/compose.yaml up --detach --wait --scale app=3 app -docker compose --file deployment/compose.yaml up --detach --wait load-balancer +docker compose --file deployment/compose.yaml up --detach --wait --scale app=3 load-balancer curl --fail http://127.0.0.1:8080/status.php ``` -Start the first app container before scaling it so initial setup completes and becomes healthy before additional instances join. The status endpoint should report `"installed": true` before treating the stack as ready. +Start the first app container before scaling it so initial setup completes and becomes healthy before additional instances join. Pass `--scale app=3` when starting the load balancer too; each Compose `up` reconciles the requested replica count, so omitting the scale option resets the service to one instance. The status endpoint should report `"installed": true` before treating the stack as ready. The HAProxy frontend discovers up to three app containers through Docker's internal DNS. To add more instances, update the HAProxy `server-template` slot count and the scale command together.