# Nextcloud Cloud Storage Role Self-contained Nextcloud deployment using Podman Quadlet with FPM, PostgreSQL database, and Valkey cache via Unix sockets. ## Features - **Container**: Single Nextcloud FPM container via Podman Quadlet - **Database**: Self-managed PostgreSQL database via Unix socket - **Cache**: Valkey (Redis-compatible) for file locking and caching - **Web Server**: Caddy reverse proxy with FastCGI and automatic HTTPS - **Security**: Group-based socket access, separated data/config volumes - **Size**: ~320MB FPM image (vs 1.1GB Apache variant) ## Architecture ``` Internet → Caddy (HTTPS:443) → FastCGI → Nextcloud FPM Container (127.0.0.1:9000) ↓ ↓ Serves static files PostgreSQL (socket) from /opt/nextcloud/html Valkey (socket) ``` ### Volume Layout ``` /opt/nextcloud/ ├── html/ # Application code (755 - readable by Caddy for static files) ├── data/ # User files (700 - private to container) ├── config/ # Config with secrets (700 - private to container) ├── custom_apps/ # Installed apps (755 - readable) └── .env # Environment variables (600) ``` **Security Model**: - Caddy serves static assets (CSS/JS/images) directly from `/opt/nextcloud/html` - Caddy cannot access `/data` or `/config` (mode 700) - User files are only served through authenticated PHP requests via FPM ## Dependencies - `postgresql` role (infrastructure) - `valkey` role (infrastructure) - `caddy` role (web server) - `podman` role (container runtime) ## Variables See `defaults/main.yml` for all configurable variables. ### Required Vault Variables Define these in your `host_vars/` with `ansible-vault`: ```yaml # Core credentials (required) vault_nextcloud_db_password: "secure-database-password" vault_nextcloud_admin_password: "secure-admin-password" vault_valkey_password: "secure-valkey-password" # Email credentials (optional - only if email enabled) vault_nextcloud_smtp_password: "secure-smtp-password" # OIDC credentials (optional - only if OIDC enabled) vault_nextcloud_oidc_client_id: "nextcloud-client-id-from-authentik" vault_nextcloud_oidc_client_secret: "nextcloud-client-secret-from-authentik" ``` ### Key Variables ```yaml # Domain nextcloud_domain: "cloud.jnss.me" # Admin user nextcloud_admin_user: "admin" # Database nextcloud_db_name: "nextcloud" nextcloud_db_user: "nextcloud" # Cache (use different DB number per service) nextcloud_valkey_db: 2 # Authentik uses 1 # PHP limits nextcloud_php_memory_limit: "512M" nextcloud_php_upload_limit: "512M" ``` ## Deployment Strategy This role uses a **two-phase deployment** approach to work correctly with the Nextcloud container's initialization process: ### Phase 1: Container Initialization (automatic) 1. Create empty directories for volumes 2. Deploy environment configuration (`.env`) 3. Start Nextcloud container 4. Container entrypoint detects first-time setup (no `version.php`) 5. Container copies Nextcloud files to `/var/www/html/` 6. Container runs `occ maintenance:install` with PostgreSQL 7. Installation creates `config.php` with database credentials ### Phase 2: Configuration via OCC Script (automatic) 8. Ansible waits for `occ status` to report `installed: true` 9. Ansible deploys and runs configuration script inside container 10. Script configures system settings via OCC commands: - Redis caching (without sessions) - Maintenance window and phone region - Database optimizations (indices, bigint, mimetypes) **Why this order?** The Nextcloud container's entrypoint uses `version.php` as a marker to determine if installation is needed. We must wait for the container's auto-installation to complete before running configuration commands: - Container must complete first-time setup (copy files, run `occ maintenance:install`) - OCC commands require a fully initialized Nextcloud installation - Running configuration after installation avoids conflicts with the entrypoint script **Configuration Method:** This role uses **OCC commands via a script** rather than config files because: - ✅ **Explicit and verifiable** - Run `occ config:list system` to see exact state - ✅ **No file conflicts** - Avoids issues with Docker image's built-in config files - ✅ **Fully idempotent** - Safe to re-run during updates - ✅ **Single source of truth** - All configuration in one script template See the official [Nextcloud Docker documentation](https://github.com/nextcloud/docker#auto-configuration-via-environment-variables) for more details on the auto-configuration process. ## Installed Apps This role automatically installs and enables the following apps: - **user_oidc** - OpenID Connect authentication backend for SSO integration - **calendar** - Calendar and scheduling application (CalDAV) - **contacts** - Contact management application (CardDAV) To customize the app list, override these variables in your `host_vars`: ```yaml nextcloud_apps_install: - user_oidc - calendar - contacts - tasks # Add more apps as needed - deck - mail ``` ## OIDC/SSO Integration ### Prerequisites Before enabling OIDC, you must create an OIDC application/provider in your identity provider (e.g., Authentik): **For Authentik:** 1. Navigate to **Applications → Providers** 2. Click **Create** → **OAuth2/OpenID Provider** 3. Configure: - **Name**: `Nextcloud` - **Authorization flow**: `default-authentication-flow` (or your preferred flow) - **Client type**: `Confidential` - **Client ID**: Generate or specify (save this) - **Client Secret**: Generate or specify (save this) - **Redirect URIs**: `https://cloud.jnss.me/apps/user_oidc/code` - **Signing Key**: Select your signing certificate - **Scopes**: Add `openid`, `profile`, `email` 4. Create **Application**: - Navigate to **Applications → Applications** - Click **Create** - **Name**: `Nextcloud` - **Slug**: `nextcloud` - **Provider**: Select the provider created above - **Launch URL**: `https://cloud.jnss.me` 5. Note the **Discovery URL**: `https://auth.jnss.me/application/o/nextcloud/.well-known/openid-configuration` ### Configuration Enable OIDC in your `host_vars/arch-vps/main.yml`: ```yaml # OIDC Configuration nextcloud_oidc_enabled: true nextcloud_oidc_provider_id: "authentik" # Provider identifier (slug) nextcloud_oidc_provider_name: "Authentik SSO" # Display name on login button nextcloud_oidc_discovery_url: "https://auth.jnss.me/application/o/nextcloud/.well-known/openid-configuration" # Security settings (recommended defaults) nextcloud_oidc_unique_uid: true # Prevents account takeover between providers nextcloud_oidc_check_bearer: false nextcloud_oidc_send_id_token_hint: true # Attribute mappings (defaults work for most providers) nextcloud_oidc_mapping_display_name: "name" nextcloud_oidc_mapping_email: "email" nextcloud_oidc_mapping_uid: "preferred_username" # Or "sub" for UUID # Optional: Enable single login (auto-redirect to SSO) nextcloud_oidc_single_login: false # Set to true to force SSO login ``` Add credentials to your vault file `host_vars/arch-vps/vault.yml`: ```yaml vault_nextcloud_oidc_client_id: "nextcloud-client-id-from-authentik" vault_nextcloud_oidc_client_secret: "nextcloud-client-secret-from-authentik" ``` ### OIDC Scopes The following scopes are requested from your OIDC provider by default: ```yaml nextcloud_oidc_scope: "email profile nextcloud openid" ``` **Standard scopes:** - `openid` - Required for OpenID Connect (contains no claims itself) - `email` - User's email address (`email` and `email_verified` claims) - `profile` - User's profile information (`name`, `given_name`, `preferred_username`, `picture`, etc.) **Custom scope for Authentik:** - `nextcloud` - Custom scope mapping you create in Authentik (contains `groups`, `quota`, `user_id`) #### Creating the Nextcloud Scope Mapping in Authentik The `nextcloud` scope must be created as a custom property mapping in Authentik: 1. Log in to Authentik as administrator 2. Navigate to **Customization** → **Property mappings** → **Create** 3. Select type: **Scope mapping** 4. Configure: - **Name**: `Nextcloud Profile` - **Scope name**: `nextcloud` - **Expression**: ```python # Extract all groups the user is a member of groups = [group.name for group in user.ak_groups.all()] # In Nextcloud, administrators must be members of a fixed group called "admin" # If a user is an admin in authentik, ensure that "admin" is appended to their group list if user.is_superuser and "admin" not in groups: groups.append("admin") return { "name": request.user.name, "groups": groups, # Set a quota by using the "nextcloud_quota" property in the user's attributes "quota": user.group_attributes().get("nextcloud_quota", None), # To connect an existing Nextcloud user, set "nextcloud_user_id" to the Nextcloud username "user_id": user.attributes.get("nextcloud_user_id", str(user.uuid)), } ``` 5. Click **Finish** 6. Navigate to your Nextcloud provider → **Advanced protocol settings** 7. Add `Nextcloud Profile` to **Scopes** (in addition to the default scopes) ### Group Provisioning and Synchronization Automatically sync user group membership from Authentik to Nextcloud. **Default configuration:** ```yaml nextcloud_oidc_group_provisioning: true # Auto-create groups from Authentik nextcloud_oidc_mapping_groups: "groups" # Claim containing group list ``` **How it works:** 1. User logs in via OIDC 2. Authentik sends group membership in the `groups` claim (from the custom scope) 3. Nextcloud automatically: - Creates groups that don't exist in Nextcloud - Adds user to those groups - Removes user from groups they're no longer member of in Authentik **Example: Making a user an admin** Nextcloud requires admins to be in a group literally named `admin`. The custom scope mapping (above) automatically adds `"admin"` to the groups list for Authentik superusers. Alternatively, manually create a group in Authentik called `admin` and add users to it. **Quota management:** Set storage quotas by adding the `nextcloud_quota` attribute to Authentik groups or users: 1. In Authentik, navigate to **Directory** → **Groups** → select your group 2. Under **Attributes**, add: ```json { "nextcloud_quota": "15 GB" } ``` 3. Users in this group will have a 15 GB quota in Nextcloud 4. If not set, quota is unlimited ### Complete Authentik Setup Guide Follow these steps to set up OIDC authentication with Authentik: **Step 1: Create the Custom Scope Mapping** See [Creating the Nextcloud Scope Mapping in Authentik](#creating-the-nextcloud-scope-mapping-in-authentik) above. **Step 2: Create the OAuth2/OpenID Provider** 1. In Authentik, navigate to **Applications** → **Providers** 2. Click **Create** → **OAuth2/OpenID Provider** 3. Configure: - **Name**: `Nextcloud` - **Authorization flow**: `default-authentication-flow` (or your preferred flow) - **Client type**: `Confidential` - **Client ID**: Generate or specify (save this for later) - **Client Secret**: Generate or specify (save this for later) - **Redirect URIs**: `https://cloud.jnss.me/apps/user_oidc/code` - **Signing Key**: Select your signing certificate - Under **Advanced protocol settings**: - **Scopes**: Add `openid`, `email`, `profile`, and `Nextcloud Profile` (the custom scope created in Step 1) - **Subject mode**: `Based on the User's UUID` (or `Based on the User's username` if you prefer usernames) **Step 3: Create the Application** 1. Navigate to **Applications** → **Applications** 2. Click **Create** 3. Configure: - **Name**: `Nextcloud` - **Slug**: `nextcloud` - **Provider**: Select the provider created in Step 2 - **Launch URL**: `https://cloud.jnss.me` (optional) **Step 4: Note the Discovery URL** The discovery URL follows this pattern: ``` https://auth.jnss.me/application/o//.well-known/openid-configuration ``` For the application slug `nextcloud`, it will be: ``` https://auth.jnss.me/application/o/nextcloud/.well-known/openid-configuration ``` **Step 5: Configure Nextcloud Role Variables** In your `host_vars/arch-vps/main.yml`: ```yaml nextcloud_oidc_enabled: true nextcloud_oidc_provider_id: "authentik" nextcloud_oidc_provider_name: "Authentik" nextcloud_oidc_discovery_url: "https://auth.jnss.me/application/o/nextcloud/.well-known/openid-configuration" nextcloud_oidc_scope: "email profile nextcloud openid" nextcloud_oidc_mapping_uid: "preferred_username" # Or "sub" for UUID-based IDs nextcloud_oidc_mapping_display_name: "name" nextcloud_oidc_mapping_email: "email" nextcloud_oidc_mapping_groups: "groups" nextcloud_oidc_mapping_quota: "quota" nextcloud_oidc_group_provisioning: true ``` In your `host_vars/arch-vps/vault.yml`: ```yaml vault_nextcloud_oidc_client_id: "nextcloud" # Client ID from Authentik vault_nextcloud_oidc_client_secret: "very-long-secret-from-authentik" # Client Secret from Authentik ``` **Step 6: Deploy and Test** Run the Nextcloud playbook: ```bash ansible-playbook -i inventory/hosts.yml site.yml --tags nextcloud --ask-vault-pass ``` ### Supported OIDC Providers The `user_oidc` app supports any **OpenID Connect 1.0** compliant provider: - **Authentik** (recommended for self-hosted) - **Keycloak** - **Auth0** - **Okta** - **Azure AD / Microsoft Entra ID** - **Google Identity Platform** - **GitHub** (via OIDC) - **GitLab** - **Authelia** - **Kanidm** - Any other OIDC 1.0 compliant provider The `nextcloud_oidc_provider_id` is just an identifier slug - you can use any value like `authentik`, `keycloak`, `auth0`, `mycompany-sso`, etc. ### Verification After deployment: 1. **Check provider configuration:** ```bash podman exec --user www-data nextcloud php occ user_oidc:provider podman exec --user www-data nextcloud php occ user_oidc:provider authentik ``` 2. **Test login:** - Visit `https://cloud.jnss.me` - You should see a "Log in with Authentik SSO" button - Click it to test SSO flow - User account should be auto-created on first login 3. **Check user mapping:** ```bash podman exec --user www-data nextcloud php occ user:list ``` ### Troubleshooting OIDC **Login button doesn't appear:** ```bash # Check if user_oidc app is enabled podman exec --user www-data nextcloud php occ app:list | grep user_oidc # Enable if needed podman exec --user www-data nextcloud php occ app:enable user_oidc ``` **Discovery URL errors:** ```bash # Test discovery URL is accessible from container podman exec nextcloud curl -k https://auth.jnss.me/application/o/nextcloud/.well-known/openid-configuration ``` **JWKS cache issues:** ```bash # Clear JWKS cache podman exec --user www-data nextcloud php occ user_oidc:provider authentik \ --clientid='your-client-id' ``` ## Email Configuration Configure Nextcloud to send emails for password resets, notifications, and sharing. ### Configuration Enable email in your `host_vars/arch-vps/main.yml`: ```yaml # Email Configuration nextcloud_email_enabled: true nextcloud_smtp_host: "smtp.fastmail.com" nextcloud_smtp_port: 587 nextcloud_smtp_secure: "tls" # tls, ssl, or empty nextcloud_smtp_username: "nextcloud@jnss.me" nextcloud_mail_from_address: "nextcloud" nextcloud_mail_domain: "jnss.me" # Set admin user's email address nextcloud_admin_email: "admin@jnss.me" ``` Add SMTP password to vault `host_vars/arch-vps/vault.yml`: ```yaml vault_nextcloud_smtp_password: "your-smtp-app-password" ``` ### Common SMTP Providers **Fastmail:** ```yaml nextcloud_smtp_host: "smtp.fastmail.com" nextcloud_smtp_port: 587 nextcloud_smtp_secure: "tls" ``` **Gmail (App Password required):** ```yaml nextcloud_smtp_host: "smtp.gmail.com" nextcloud_smtp_port: 587 nextcloud_smtp_secure: "tls" ``` **Office 365:** ```yaml nextcloud_smtp_host: "smtp.office365.com" nextcloud_smtp_port: 587 nextcloud_smtp_secure: "tls" ``` **SMTP2GO:** ```yaml nextcloud_smtp_host: "mail.smtp2go.com" nextcloud_smtp_port: 587 nextcloud_smtp_secure: "tls" ``` ### Verification After deployment: 1. **Check SMTP configuration:** ```bash podman exec --user www-data nextcloud php occ config:list system | grep mail ``` 2. **Check admin email:** ```bash podman exec --user www-data nextcloud php occ user:setting admin settings email ``` 3. **Send test email via Web UI:** - Log in as admin - Settings → Administration → Basic settings - Scroll to "Email server" - Click "Send email" button - Check recipient inbox ### Troubleshooting Email **Test SMTP connection from container:** ```bash # Install swaks if needed (for testing) podman exec nextcloud apk add --no-cache swaks # Test SMTP connection podman exec nextcloud swaks \ --to recipient@example.com \ --from nextcloud@jnss.me \ --server smtp.fastmail.com:587 \ --auth LOGIN \ --auth-user nextcloud@jnss.me \ --auth-password 'your-password' \ --tls ``` **Check Nextcloud logs:** ```bash podman exec --user www-data nextcloud php occ log:watch ``` ## Usage ### Include in Playbook ```yaml - role: nextcloud tags: ['nextcloud', 'cloud', 'storage'] ``` ### Deploy ```bash # Deploy Nextcloud role ansible-playbook -i inventory/hosts.yml site.yml --tags nextcloud --ask-vault-pass # Deploy only infrastructure dependencies ansible-playbook -i inventory/hosts.yml site.yml --tags postgresql,valkey,caddy ``` ## Verification After deployment: 1. **Access Nextcloud**: ```bash https://cloud.jnss.me ``` 2. **Check service status**: ```bash ssh root@arch-vps systemctl status nextcloud podman ps | grep nextcloud ``` 3. **View logs**: ```bash # Container logs journalctl -u nextcloud -f podman logs nextcloud # Caddy logs tail -f /var/log/caddy/nextcloud.log ``` 4. **Verify socket access**: ```bash # Check group memberships id nextcloud # Should show: postgres-clients, valkey-clients # Check socket permissions ls -la /var/run/postgresql/.s.PGSQL.5432 ls -la /var/run/valkey/valkey.sock ``` ## Maintenance ### OCC Command (Nextcloud CLI) Run Nextcloud's OCC command-line tool: ```bash # General syntax podman exec --user www-data nextcloud php occ # Examples podman exec --user www-data nextcloud php occ status podman exec --user www-data nextcloud php occ app:list podman exec --user www-data nextcloud php occ maintenance:mode --on podman exec --user www-data nextcloud php occ files:scan --all ``` ### Update Nextcloud The container automatically updates on restart: ```bash systemctl restart nextcloud ``` Or pull specific version: ```yaml # In host_vars or defaults nextcloud_version: "32-fpm" # Pin to major version # Or nextcloud_version: "32.0.3-fpm" # Pin to exact version ``` ### Backup Strategy Key directories to backup: 1. **User data**: `/opt/nextcloud/data` 2. **Configuration**: `/opt/nextcloud/config` 3. **Database**: PostgreSQL `nextcloud` database 4. **Custom apps**: `/opt/nextcloud/custom_apps` (optional) Example backup script: ```bash #!/bin/bash # Enable maintenance mode podman exec --user www-data nextcloud php occ maintenance:mode --on # Backup data and config tar -czf nextcloud-data-$(date +%Y%m%d).tar.gz /opt/nextcloud/data /opt/nextcloud/config # Backup database sudo -u postgres pg_dump nextcloud > nextcloud-db-$(date +%Y%m%d).sql # Disable maintenance mode podman exec --user www-data nextcloud php occ maintenance:mode --off ``` ### Performance Tuning Adjust PHP limits in `host_vars`: ```yaml nextcloud_php_memory_limit: "1G" # For large files nextcloud_php_upload_limit: "10G" # For large uploads ``` ### Redis/Valkey Caching Architecture This role uses a **split caching strategy** for optimal performance and stability: **PHP Sessions**: File-based (default PHP session handler) - Location: `/var/www/html/data/sessions/` - Why: Redis session locking can cause cascading failures under high concurrency - Performance: Excellent for single-server deployments **Nextcloud Application Cache**: Redis/Valkey - `memcache.local`: APCu (in-memory opcode cache) - `memcache.distributed`: Redis (shared cache, file locking) - `memcache.locking`: Redis (transactional file locking) - Configuration: Via OCC commands in configuration script **Why not Redis sessions?** The official Nextcloud Docker image enables Redis session handling when `REDIS_HOST` is set. However, this can cause severe performance issues: 1. **Session lock contention**: Multiple parallel requests (browser loading CSS/JS/images) compete for the same session lock 2. **Infinite retries**: Default `lock_retries = -1` means workers block forever 3. **Timeout orphaning**: When reverse proxy times out, FPM workers keep running and hold locks 4. **Worker exhaustion**: Limited FPM workers (default 5) all become blocked 5. **Cascading failure**: New requests queue, timeouts accumulate, locks orphan This role disables Redis sessions by **not setting** `REDIS_HOST` in the environment, while still providing Redis caching via OCC configuration commands. **If you need Redis sessions** (e.g., multi-server setup with session sharing), you must: 1. Enable `REDIS_HOST` in `nextcloud.env.j2` 2. Add a custom PHP ini file with proper lock parameters: - `redis.session.lock_expire = 30` (locks expire after 30 seconds) - `redis.session.lock_retries = 100` (max 100 retries, not infinite) - `redis.session.lock_wait_time = 50000` (50ms between retries) 3. Mount the ini file with `zz-` prefix to load after the entrypoint's redis-session.ini 4. Increase FPM workers significantly (15-20+) 5. Monitor for orphaned session locks ## Troubleshooting ### Container won't start ```bash # Check container logs journalctl -u nextcloud -n 50 podman logs nextcloud # Check systemd unit systemctl status nextcloud ``` ### Permission errors ```bash # Verify user groups id nextcloud # Should be in: postgres-clients, valkey-clients # If not, re-run user.yml tasks: ansible-playbook -i inventory/hosts.yml site.yml --tags nextcloud,user ``` ### Database connection errors ```bash # Test PostgreSQL socket sudo -u nextcloud psql -h /var/run/postgresql -U nextcloud -d nextcloud # Check socket exists and permissions ls -la /var/run/postgresql/.s.PGSQL.5432 ``` ### Caddy FastCGI errors ```bash # Check Caddy can read app files sudo -u caddy ls -la /opt/nextcloud/html # Verify FPM is listening ss -tlnp | grep 9000 # Test FPM connection curl -v http://127.0.0.1:9000 ``` ### "Trusted domain" errors Add domains to `nextcloud_trusted_domains`: ```yaml nextcloud_trusted_domains: "cloud.jnss.me localhost 69.62.119.31" ``` Or add via OCC: ```bash podman exec --user www-data nextcloud php occ config:system:set trusted_domains 1 --value=cloud.jnss.me ``` ## Integration with Authentik SSO To integrate Nextcloud with Authentik for SSO, see the Authentik documentation for OAuth2/OIDC provider setup. ## Security Notes - User data (`/opt/nextcloud/data`) is mode 700 - only container can access - Config (`/opt/nextcloud/config`) is mode 700 - contains database passwords - Application files (`/opt/nextcloud/html`) are mode 755 - Caddy can read for static files - All traffic is HTTPS via Caddy with automatic Let's Encrypt certificates - Database and cache connections use Unix sockets (no TCP exposure) - Container runs as root initially, then switches to www-data (UID 33) for PHP-FPM ### Socket Access Pattern Nextcloud uses a different access pattern than other rick-infra services due to how the official Nextcloud container works: **How it works:** 1. Container starts as root (UID 0) 2. Entrypoint runs as root to write PHP configuration files 3. Entrypoint switches to www-data (UID 33) for PHP-FPM process 4. www-data accesses PostgreSQL and Valkey via Unix sockets **Why 777 socket permissions are needed:** - The Nextcloud container cannot use `--group-add` effectively because: - `--group-add` only adds groups to the **initial user** (root) - When the container switches from root to www-data, supplementary groups are lost - www-data (UID 33, GID 33) ends up with no access to group-restricted sockets - Infrastructure sockets use mode 777 to allow access by any UID - Security is maintained via password authentication (PostgreSQL: scram-sha-256, Valkey: requirepass) - Sockets are local-only (not network-exposed) **Alternative (TCP)**: If you prefer group-based socket access (770), you can configure PostgreSQL and Valkey to use TCP instead: ```yaml # In host_vars postgresql_listen_addresses: "127.0.0.1" postgresql_unix_socket_permissions: "0770" # Restrict to group valkey_bind: "127.0.0.1" valkey_port: 6379 valkey_unix_socket_enabled: false # In Nextcloud env POSTGRES_HOST=127.0.0.1 POSTGRES_PORT=5432 REDIS_HOST=127.0.0.1 REDIS_PORT=6379 ``` This provides the same security level (password-authenticated, localhost-only) but uses TCP instead of Unix sockets. The trade-off is slightly lower performance compared to Unix sockets. See infrastructure role documentation (PostgreSQL and Valkey READMEs) for more details on this architectural decision. ## References - [Nextcloud Official Docker Image](https://hub.docker.com/_/nextcloud) - [Nextcloud Documentation](https://docs.nextcloud.com/) - [Caddy FastCGI Documentation](https://caddyserver.com/docs/caddyfile/directives/php_fastcgi)