Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/CI-CD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!--
SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: MIT
-->

# 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.
84 changes: 84 additions & 0 deletions .github/workflows/deployment-architecture.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 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 load-balancer

- name: Verify load-balanced Nextcloud and three app servers
run: |
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'

- 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions deployment/.env.example
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions deployment/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!--
SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-->

# 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 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. 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.

## 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.
53 changes: 53 additions & 0 deletions deployment/backup.sh
Original file line number Diff line number Diff line change
@@ -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
91 changes: 91 additions & 0 deletions deployment/compose.yaml
Original file line number Diff line number Diff line change
@@ -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:
Loading