From 93ce586b9444c361e6d07a367d56d448fc0f06bc Mon Sep 17 00:00:00 2001 From: Pieter Date: Tue, 6 Jan 2026 09:30:54 +0100 Subject: [PATCH] Deploy Nextcloud file sync/share with automated installation (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements a complete Nextcloud deployment with PostgreSQL, Redis, automated installation, and preparation for OIDC/SSO integration with Zitadel. ## Nextcloud Deployment ### New Ansible Role (ansible/roles/nextcloud/) - Complete Nextcloud v30 deployment with Docker Compose - PostgreSQL 16 backend with persistent volumes - Redis 7 for caching and file locking - Automated installation via Docker environment variables - Post-installation configuration via occ commands ### Features Implemented - **Database**: PostgreSQL with proper credentials and persistence - **Caching**: Redis for memory caching and file locking - **HTTPS**: Traefik integration with Let's Encrypt SSL - **Security**: Proper security headers and HSTS - **WebDAV**: CalDAV/CardDAV redirect middleware - **Configuration**: Automated trusted domain, reverse proxy, and Redis setup - **OIDC Preparation**: user_oidc app installed and enabled ### Traefik Updates - Added Nextcloud routing to dynamic.yml (static file-based config) - Configured CalDAV/CardDAV redirect middleware - Added Nextcloud-specific security headers ### Configuration Tasks - Automated trusted domain configuration for nextcloud.test.vrije.cloud - Reverse proxy overwrite settings (protocol, host, CLI URL) - Redis cache and locking configuration - Default phone region (NL) - Background jobs via cron ## Deployment Status ✅ Successfully deployed and tested: - Nextcloud: https://nextcloud.test.vrije.cloud/ - Admin login working - PostgreSQL database initialized - Redis caching operational - HTTPS with Let's Encrypt SSL - user_oidc app installed (ready for Zitadel integration) ## Next Steps To complete OIDC/SSO integration: 1. Create OIDC application in Zitadel console 2. Use redirect URI: https://nextcloud.test.vrije.cloud/apps/user_oidc/code 3. Configure provider in Nextcloud with Zitadel credentials Partially addresses #4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ansible/playbooks/deploy.yml | 13 +- ansible/roles/nextcloud/defaults/main.yml | 36 ++++++ ansible/roles/nextcloud/handlers/main.yml | 7 ++ ansible/roles/nextcloud/tasks/docker.yml | 29 +++++ ansible/roles/nextcloud/tasks/install.yml | 43 +++++++ ansible/roles/nextcloud/tasks/main.yml | 21 ++++ ansible/roles/nextcloud/tasks/oidc.yml | 36 ++++++ .../templates/docker-compose.nextcloud.yml.j2 | 112 ++++++++++++++++++ .../roles/traefik/templates/dynamic.yml.j2 | 32 +++++ 9 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 ansible/roles/nextcloud/defaults/main.yml create mode 100644 ansible/roles/nextcloud/handlers/main.yml create mode 100644 ansible/roles/nextcloud/tasks/docker.yml create mode 100644 ansible/roles/nextcloud/tasks/install.yml create mode 100644 ansible/roles/nextcloud/tasks/main.yml create mode 100644 ansible/roles/nextcloud/tasks/oidc.yml create mode 100644 ansible/roles/nextcloud/templates/docker-compose.nextcloud.yml.j2 diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml index 2f657cb..4da5c09 100644 --- a/ansible/playbooks/deploy.yml +++ b/ansible/playbooks/deploy.yml @@ -26,8 +26,14 @@ zitadel_domain: "{{ client_secrets.zitadel_domain }}" when: client_secrets.zitadel_domain is defined + - name: Set client domain from secrets + set_fact: + client_domain: "{{ client_secrets.client_domain }}" + when: client_secrets.client_domain is defined + roles: - role: zitadel + - role: nextcloud post_tasks: - name: Display deployment summary @@ -36,8 +42,11 @@ Deployment complete for client: {{ client_name }} Zitadel: https://{{ zitadel_domain }} + Nextcloud: https://nextcloud.{{ client_domain }} Next steps: 1. Login to Zitadel with the admin credentials - 2. Change the admin password - 3. Configure OIDC applications for Nextcloud (when deployed) + 2. Create OIDC application in Zitadel for Nextcloud + - Client name: Nextcloud + - Redirect URI: https://nextcloud.{{ client_domain }}/apps/user_oidc/code + 3. Configure OIDC in Nextcloud using the client ID and secret from Zitadel diff --git a/ansible/roles/nextcloud/defaults/main.yml b/ansible/roles/nextcloud/defaults/main.yml new file mode 100644 index 0000000..7d2c51c --- /dev/null +++ b/ansible/roles/nextcloud/defaults/main.yml @@ -0,0 +1,36 @@ +--- +# Default variables for nextcloud role + +# Nextcloud version +nextcloud_version: "30" # Latest stable version (uses major version tag) + +# Database configuration +nextcloud_db_type: "pgsql" +nextcloud_db_host: "nextcloud-db" +nextcloud_db_port: "5432" +nextcloud_db_name: "nextcloud" +nextcloud_db_user: "nextcloud" + +# Admin user configuration +nextcloud_admin_user: "admin" + +# Nextcloud domain (will be set from client_domain variable) +nextcloud_domain: "nextcloud.{{ client_domain }}" + +# Redis configuration for caching and file locking +nextcloud_redis_host: "nextcloud-redis" +nextcloud_redis_port: "6379" + +# OIDC configuration +nextcloud_oidc_enabled: true +nextcloud_oidc_provider_url: "https://{{ zitadel_domain }}" +nextcloud_oidc_client_id: "" # Will be set after creating app in Zitadel +nextcloud_oidc_client_secret: "" # Will be set after creating app in Zitadel + +# Trusted domains (for Nextcloud config) +nextcloud_trusted_domains: + - "{{ nextcloud_domain }}" + +# PHP memory limit +nextcloud_php_memory_limit: "512M" +nextcloud_php_upload_limit: "16G" diff --git a/ansible/roles/nextcloud/handlers/main.yml b/ansible/roles/nextcloud/handlers/main.yml new file mode 100644 index 0000000..80fb863 --- /dev/null +++ b/ansible/roles/nextcloud/handlers/main.yml @@ -0,0 +1,7 @@ +--- +# Handlers for Nextcloud role + +- name: Restart Nextcloud + community.docker.docker_compose_v2: + project_src: /opt/nextcloud + state: restarted diff --git a/ansible/roles/nextcloud/tasks/docker.yml b/ansible/roles/nextcloud/tasks/docker.yml new file mode 100644 index 0000000..c0a2daa --- /dev/null +++ b/ansible/roles/nextcloud/tasks/docker.yml @@ -0,0 +1,29 @@ +--- +# Docker deployment tasks for Nextcloud + +- name: Create Nextcloud directory + file: + path: /opt/nextcloud + state: directory + mode: '0755' + +- name: Deploy Nextcloud Docker Compose file + template: + src: docker-compose.nextcloud.yml.j2 + dest: /opt/nextcloud/docker-compose.yml + mode: '0600' + notify: Restart Nextcloud + +- name: Start Nextcloud services + community.docker.docker_compose_v2: + project_src: /opt/nextcloud + state: present + register: nextcloud_deploy + +- name: Wait for Nextcloud to be ready + wait_for: + host: localhost + port: 80 + delay: 10 + timeout: 120 + when: nextcloud_deploy.changed diff --git a/ansible/roles/nextcloud/tasks/install.yml b/ansible/roles/nextcloud/tasks/install.yml new file mode 100644 index 0000000..ef8c4a2 --- /dev/null +++ b/ansible/roles/nextcloud/tasks/install.yml @@ -0,0 +1,43 @@ +--- +# Automated Nextcloud installation tasks using occ commands + +- name: Wait for Nextcloud container to be healthy + shell: docker exec -u www-data nextcloud php -v + register: nextcloud_health + retries: 30 + delay: 10 + until: nextcloud_health.rc == 0 + changed_when: false + +- name: Wait for Nextcloud auto-installation to complete + shell: "docker exec -u www-data nextcloud php occ status 2>&1 | grep -q 'installed: true'" + register: nextcloud_status + retries: 60 + delay: 5 + until: nextcloud_status.rc == 0 + changed_when: false + +- name: Configure trusted domains + shell: | + docker exec -u www-data nextcloud php occ config:system:set trusted_domains 0 --value="{{ nextcloud_domain }}" + +- name: Configure overwrite settings for reverse proxy + shell: | + docker exec -u www-data nextcloud php occ config:system:set overwriteprotocol --value="https" + docker exec -u www-data nextcloud php occ config:system:set overwritehost --value="{{ nextcloud_domain }}" + docker exec -u www-data nextcloud php occ config:system:set overwrite.cli.url --value="https://{{ nextcloud_domain }}" + +- name: Configure Redis for caching + shell: | + docker exec -u www-data nextcloud php occ config:system:set redis host --value="{{ nextcloud_redis_host }}" + docker exec -u www-data nextcloud php occ config:system:set redis port --value="{{ nextcloud_redis_port }}" + docker exec -u www-data nextcloud php occ config:system:set memcache.local --value="\OC\Memcache\Redis" + docker exec -u www-data nextcloud php occ config:system:set memcache.locking --value="\OC\Memcache\Redis" + +- name: Set default phone region + shell: | + docker exec -u www-data nextcloud php occ config:system:set default_phone_region --value="NL" + +- name: Run background jobs via cron + shell: | + docker exec -u www-data nextcloud php occ background:cron diff --git a/ansible/roles/nextcloud/tasks/main.yml b/ansible/roles/nextcloud/tasks/main.yml new file mode 100644 index 0000000..e6aed24 --- /dev/null +++ b/ansible/roles/nextcloud/tasks/main.yml @@ -0,0 +1,21 @@ +--- +# Main tasks for Nextcloud deployment + +- name: Include Docker deployment tasks + include_tasks: docker.yml + tags: + - nextcloud + - docker + +- name: Include installation tasks + include_tasks: install.yml + tags: + - nextcloud + - install + +- name: Include OIDC configuration tasks + include_tasks: oidc.yml + when: nextcloud_oidc_enabled | default(true) + tags: + - nextcloud + - oidc diff --git a/ansible/roles/nextcloud/tasks/oidc.yml b/ansible/roles/nextcloud/tasks/oidc.yml new file mode 100644 index 0000000..f8b8443 --- /dev/null +++ b/ansible/roles/nextcloud/tasks/oidc.yml @@ -0,0 +1,36 @@ +--- +# OIDC/SSO integration tasks for Nextcloud with Zitadel + +- name: Check if user_oidc app is installed + shell: docker exec -u www-data nextcloud php occ app:list --output=json + register: nextcloud_apps + changed_when: false + +- name: Parse installed apps + set_fact: + user_oidc_installed: "{{ 'user_oidc' in (nextcloud_apps.stdout | from_json).enabled }}" + +- name: Install user_oidc app + shell: docker exec -u www-data nextcloud php occ app:install user_oidc + when: not user_oidc_installed + register: oidc_install + changed_when: "'installed' in oidc_install.stdout" + +- name: Enable user_oidc app + shell: docker exec -u www-data nextcloud php occ app:enable user_oidc + when: not user_oidc_installed + +# Note: OIDC provider configuration requires the Zitadel application to be created first +# This will be configured manually or via Zitadel API in a follow-up task +- name: Display OIDC configuration instructions + debug: + msg: | + To complete OIDC setup: + 1. Create an OIDC application in Zitadel console at https://{{ zitadel_domain }} + 2. Use redirect URI: https://{{ nextcloud_domain }}/apps/user_oidc/code + 3. Configure the provider in Nextcloud using: + docker exec -u www-data nextcloud php occ user_oidc:provider:add \ + --clientid="" \ + --clientsecret="" \ + --discoveryuri="https://{{ zitadel_domain }}/.well-known/openid-configuration" \ + "Zitadel" diff --git a/ansible/roles/nextcloud/templates/docker-compose.nextcloud.yml.j2 b/ansible/roles/nextcloud/templates/docker-compose.nextcloud.yml.j2 new file mode 100644 index 0000000..390a99c --- /dev/null +++ b/ansible/roles/nextcloud/templates/docker-compose.nextcloud.yml.j2 @@ -0,0 +1,112 @@ +services: + # PostgreSQL Database for Nextcloud + nextcloud-db: + image: postgres:16-alpine + container_name: nextcloud-db + restart: unless-stopped + volumes: + - nextcloud-db-data:/var/lib/postgresql/data + environment: + POSTGRES_DB: {{ nextcloud_db_name }} + POSTGRES_USER: {{ nextcloud_db_user }} + POSTGRES_PASSWORD: {{ client_secrets.nextcloud_db_password }} + # Grant full privileges to the user + POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256" + networks: + - nextcloud-internal + + # Redis for caching and file locking + nextcloud-redis: + image: redis:7-alpine + container_name: nextcloud-redis + restart: unless-stopped + command: redis-server --save 60 1 --loglevel warning + volumes: + - nextcloud-redis-data:/data + networks: + - nextcloud-internal + + # Nextcloud application + nextcloud: + image: nextcloud:{{ nextcloud_version }} + container_name: nextcloud + restart: unless-stopped + depends_on: + - nextcloud-db + - nextcloud-redis + volumes: + - nextcloud-data:/var/www/html + environment: + # Database configuration + POSTGRES_HOST: {{ nextcloud_db_host }} + POSTGRES_DB: {{ nextcloud_db_name }} + POSTGRES_USER: {{ nextcloud_db_user }} + POSTGRES_PASSWORD: {{ client_secrets.nextcloud_db_password }} + + # Redis configuration + REDIS_HOST: {{ nextcloud_redis_host }} + REDIS_HOST_PORT: {{ nextcloud_redis_port }} + + # Nextcloud configuration + NEXTCLOUD_ADMIN_USER: {{ nextcloud_admin_user }} + NEXTCLOUD_ADMIN_PASSWORD: {{ client_secrets.nextcloud_admin_password }} + NEXTCLOUD_TRUSTED_DOMAINS: {{ nextcloud_domain }} + OVERWRITEPROTOCOL: https + OVERWRITEHOST: {{ nextcloud_domain }} + OVERWRITECLIURL: https://{{ nextcloud_domain }} + + # PHP configuration + PHP_MEMORY_LIMIT: {{ nextcloud_php_memory_limit }} + PHP_UPLOAD_LIMIT: {{ nextcloud_php_upload_limit }} + + # SMTP configuration (optional, can be configured via OIDC later) + # SMTP_HOST: + # SMTP_SECURE: + # SMTP_PORT: + # SMTP_NAME: + # SMTP_PASSWORD: + # MAIL_FROM_ADDRESS: + # MAIL_DOMAIN: + networks: + - traefik + - nextcloud-internal + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik" + + # HTTP Router + - "traefik.http.routers.nextcloud.rule=Host(`{{ nextcloud_domain }}`)" + - "traefik.http.routers.nextcloud.entrypoints=websecure" + - "traefik.http.routers.nextcloud.tls=true" + - "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt" + + # Middleware for Nextcloud + - "traefik.http.routers.nextcloud.middlewares=nextcloud-headers,nextcloud-redirectregex" + + # Security headers + - "traefik.http.middlewares.nextcloud-headers.headers.stsSeconds=31536000" + - "traefik.http.middlewares.nextcloud-headers.headers.stsIncludeSubdomains=true" + - "traefik.http.middlewares.nextcloud-headers.headers.stsPreload=true" + + # CalDAV/CardDAV redirect + - "traefik.http.middlewares.nextcloud-redirectregex.redirectregex.permanent=true" + - "traefik.http.middlewares.nextcloud-redirectregex.redirectregex.regex=https://(.*)/.well-known/(card|cal)dav" + - "traefik.http.middlewares.nextcloud-redirectregex.redirectregex.replacement=https://$$1/remote.php/dav/" + + # Service + - "traefik.http.services.nextcloud.loadbalancer.server.port=80" + +networks: + traefik: + external: true + nextcloud-internal: + name: nextcloud-internal + driver: bridge + +volumes: + nextcloud-db-data: + name: nextcloud-db-data + nextcloud-redis-data: + name: nextcloud-redis-data + nextcloud-data: + name: nextcloud-data diff --git a/ansible/roles/traefik/templates/dynamic.yml.j2 b/ansible/roles/traefik/templates/dynamic.yml.j2 index 01bb8a1..9d567e4 100644 --- a/ansible/roles/traefik/templates/dynamic.yml.j2 +++ b/ansible/roles/traefik/templates/dynamic.yml.j2 @@ -14,6 +14,18 @@ http: middlewares: - zitadel-headers + # Nextcloud file sync/share + nextcloud: + rule: "Host(`nextcloud.test.vrije.cloud`)" + service: nextcloud + entryPoints: + - websecure + tls: + certResolver: letsencrypt + middlewares: + - nextcloud-headers + - nextcloud-redirectregex + services: # Zitadel service zitadel: @@ -21,6 +33,12 @@ http: servers: - url: "h2c://zitadel:8080" + # Nextcloud service + nextcloud: + loadBalancer: + servers: + - url: "http://nextcloud:80" + middlewares: # Zitadel-specific headers zitadel-headers: @@ -29,6 +47,20 @@ http: stsIncludeSubdomains: true stsPreload: true + # Nextcloud-specific headers + nextcloud-headers: + headers: + stsSeconds: 31536000 + stsIncludeSubdomains: true + stsPreload: true + + # CalDAV/CardDAV redirect for Nextcloud + nextcloud-redirectregex: + redirectRegex: + permanent: true + regex: "https://(.*)/.well-known/(card|cal)dav" + replacement: "https://$1/remote.php/dav/" + # Security headers security-headers: headers: