Files
kevinveenbirkenbachandClaude Opus 5.5 b17807faa3 fix(web-app-nextcloud): probe the Context Agent without curl
Context Agent 2.10.0 (bumped in #1219) moved to a multi-stage image whose
runtime stage ships no curl ("no vim, no git, no curl, no pipx/poetry",
nextcloud/context_agent Dockerfile at v2.10.0). The curl healthcheck then
fails with `exec: "curl": executable file not found in $PATH`, the task
never leaves `starting`, and the swarm converge wait gives up on
nextcloud_context_agent (main run 40401200083, Nextcloud#2).

Switch to the http_status flavor, which speaks HTTP over bash's
/dev/tcp and still requires a 2xx/3xx answer from /heartbeat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:35:30 +01:00
..
…
…

Nextcloud

Description

Elevate your collaboration with Nextcloud, a vibrant self-hosted cloud solution designed for dynamic file sharing, seamless communication, and effortless teamwork. Nextcloud offers a full suite of integrated tools (including LDAP and OIDC authentication, Redis caching, and automated plugin management via OCC) to empower a secure, extensible, and production-ready cloud environment.

Overview

This role provisions a complete Nextcloud deployment using Docker Compose. It automates the setup of the Nextcloud application along with its underlying MariaDB database and configures the system for secure public access via an NGINX reverse proxy. The deployment includes automated configuration merging into config.php, health check routines, and integrated support for backup and recovery operations.

Cosmos

The diagram places Nextcloud in the Infinito.Nexus cosmos: the components it deploys (capabilities), the central services it consumes (dependencies), and its outward reach (federation and bridged external networks).

flowchart LR
    subgraph deps [Dependencies]
        dep_svc_ai_litellm["svc-ai-litellm 🐳🐝"]
        dep_svc_bkp_volume_2_local["svc-bkp-volume-2-local 💻"]
        dep_svc_db_mariadb["svc-db-mariadb 🐳🐝"]
        dep_svc_db_openldap["svc-db-openldap 🐳🐝"]
        dep_svc_db_redis["svc-db-redis 🐳🐝"]
        dep_svc_net_tor["svc-net-tor 🐳🐝"]
        dep_web_app_bigbluebutton["web-app-bigbluebutton 🐳🐝"]
        dep_web_app_dashboard["web-app-dashboard 🐳🐝"]
        dep_web_app_discourse["web-app-discourse 🐳🐝"]
        dep_web_app_gitlab["web-app-gitlab 🐳🐝"]
        dep_web_app_hermes["web-app-hermes 🐳🐝"]
        dep_web_app_keycloak["web-app-keycloak 🐳🐝"]
        dep_web_app_mastodon["web-app-mastodon 🐳🐝"]
        dep_web_app_matomo["web-app-matomo 🐳🐝"]
        dep_web_app_matrix["web-app-matrix 🐳🐝"]
        dep_web_app_mattermost["web-app-mattermost 🐳🐝"]
        dep_web_app_openclaw["web-app-openclaw 🐳🐝"]
        dep_web_app_openproject["web-app-openproject 🐳🐝"]
        dep_web_app_openwebui["web-app-openwebui 🐳🐝"]
        dep_web_app_peertube["web-app-peertube 🐳🐝"]
        dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
        dep_web_app_seaweedfs["web-app-seaweedfs 🐳🐝"]
        dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
        dep_web_app_xwiki["web-app-xwiki 🐳🐝"]
        dep_web_app_zammad["web-app-zammad 🐳🐝"]
        dep_web_svc_collabora["web-svc-collabora 🐳🐝"]
        dep_web_svc_coturn["web-svc-coturn 🐳🐝"]
        dep_web_svc_css["web-svc-css 💻"]
        dep_web_svc_logout["web-svc-logout 🐳🐝"]
        dep_web_svc_onlyoffice["web-svc-onlyoffice 🐳🐝"]
    end
    subgraph role [web-app-nextcloud 🐳🐝]
        svc_sso["sso"]
        svc_coturn["coturn"]
        svc_logout["logout"]
        svc_ldap["ldap"]
        svc_dashboard["dashboard"]
        svc_matomo["matomo"]
        svc_email["email"]
        svc_redis["redis"]
        svc_mariadb["mariadb"]
        svc_nextcloud["nextcloud"]
        svc_proxy["proxy"]
        svc_context_agent["context_agent"]
        svc_contextagentmcp["contextagentmcp"]
        svc_cron["cron"]
        svc_talk["talk"]
        svc_whiteboard["whiteboard"]
        svc_onlyoffice["onlyoffice"]
        svc_collabora["collabora"]
        svc_bigbluebutton["bigbluebutton"]
        svc_xwiki["xwiki"]
        svc_minio["minio ❌"]
        svc_seaweedfs["seaweedfs"]
        svc_talk_recording["talk_recording"]
        svc_css["css"]
        svc_hcaptcha["hcaptcha"]
        svc_prometheus["prometheus"]
        svc_tor["tor"]
        svc_openproject["openproject"]
        svc_gitlab["gitlab"]
        svc_discourse["discourse"]
        svc_mattermost["mattermost"]
        svc_matrix["matrix"]
        svc_zammad["zammad"]
        svc_openwebui["openwebui"]
        svc_litellm["litellm"]
        svc_mastodon["mastodon"]
        svc_peertube["peertube"]
        svc_moodle["moodle ❌"]
        svc_suitecrm["suitecrm ❌"]
        svc_container_backup["container_backup"]
        svc_hermes["hermes"]
        svc_openclaw["openclaw"]
        svc_flowise["flowise ❌"]
    end
    dep_svc_ai_litellm -. "0..1" .-> svc_litellm
    dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup
    dep_svc_db_mariadb -. "0..1" .-> svc_mariadb
    dep_svc_db_openldap -. "0..1" .-> svc_ldap
    dep_svc_db_redis -. "0..1" .-> svc_redis
    dep_svc_net_tor -. "0..1" .-> svc_tor
    dep_web_app_bigbluebutton -. "0..1" .-> svc_bigbluebutton
    dep_web_app_dashboard -. "0..1" .-> svc_dashboard
    dep_web_app_discourse -. "0..1" .-> svc_discourse
    dep_web_app_gitlab -. "0..1" .-> svc_gitlab
    dep_web_app_hermes -. "0..1" .-> svc_hermes
    dep_web_app_keycloak -. "0..1" .-> svc_sso
    dep_web_app_mastodon -. "0..1" .-> svc_mastodon
    dep_web_app_matomo -. "0..1" .-> svc_matomo
    dep_web_app_matrix -. "0..1" .-> svc_matrix
    dep_web_app_mattermost -. "0..1" .-> svc_mattermost
    dep_web_app_openclaw -. "0..1" .-> svc_openclaw
    dep_web_app_openproject -. "0..1" .-> svc_openproject
    dep_web_app_openwebui -. "0..1" .-> svc_openwebui
    dep_web_app_peertube -. "0..1" .-> svc_peertube
    dep_web_app_prometheus -. "0..1" .-> svc_prometheus
    dep_web_app_seaweedfs -. "0..1" .-> svc_seaweedfs
    dep_web_app_stalwart -. "0..1" .-> svc_email
    dep_web_app_xwiki -. "0..1" .-> svc_xwiki
    dep_web_app_zammad -. "0..1" .-> svc_zammad
    dep_web_svc_collabora -. "0..1" .-> svc_collabora
    dep_web_svc_coturn -. "0..1" .-> svc_coturn
    dep_web_svc_coturn -. "0..1" .-> svc_talk
    dep_web_svc_css -. "0..1" .-> svc_css
    dep_web_svc_logout -. "0..1" .-> svc_logout
    dep_web_svc_onlyoffice -. "0..1" .-> svc_onlyoffice

Solid 1:1 edges are fixed relationships; dashed 0..1 edges are conditional (enabled only in matching deployments); red 0..0 edges are turned off in this role. Node markers show the role's deploy modes (💻 host, 🐳 compose, 🐝 swarm); ❌ marks a service that is explicitly turned off, and ⚙️ an Ansible role dependency declared in meta/main.yml.

Features

  • Fully Dockerized Deployment: Simplifies installation using Docker Compose for the Nextcloud application and its MariaDB backend.
  • Secure Access: Integrates with an NGINX reverse proxy for encrypted, high-performance access.
  • Robust Authentication: Supports LDAP and OIDC for secure identity and access management.
  • Automated Configuration Management: Uses additive configuration files to dynamically merge system settings into config.php.
  • Integrated Backup & Recovery: Provides built-in support for backup and restoration operations to safeguard your data.
  • Extensible Plugin Framework: Easily manage and configure hundreds of Nextcloud plugins using the OCC command line tool.

Quick Setup

Development

Clone, set up the workstation, and deploy Nextcloud onto the local stack:

git clone https://github.com/infinito-nexus/core.git
cd core
make onboard
make compose-deploy mode=reinstall apps=web-app-nextcloud full_cycle=false

Production

Run the published image to provision the inventory and deploy Nextcloud to a managed server (the mounted volume persists the inventory):

APP=web-app-nextcloud
HOST=<your-server>
DOMAIN=<your-domain>
TLS_MODE=self_signed
SSH_PUBLIC_KEY="<your-ssh-public-key>"

docker run --rm -it \
  -v "$PWD/inventories:/etc/infinito.nexus/inventories" \
  -e APP="$APP" -e HOST="$HOST" -e DOMAIN="$DOMAIN" -e TLS_MODE="$TLS_MODE" -e SSH_PUBLIC_KEY="$SSH_PUBLIC_KEY" \
  ghcr.io/infinito-nexus/core/debian bash -c '
    INVENTORY=/etc/infinito.nexus/inventories/production
    infinito administration inventory provision "$INVENTORY" \
      --inventory-file "$INVENTORY/devices.yml" \
      --host "$HOST" \
      --include "$APP" \
      --vars "{\"TLS_MODE\": \"$TLS_MODE\", \"DOMAIN_PRIMARY\": \"$DOMAIN\", \"users\": {\"administrator\": {\"authorized_keys\": [\"$SSH_PUBLIC_KEY\"]}}}" &&
    infinito administration deploy dedicated "$INVENTORY/devices.yml" \
      --password-file "$INVENTORY/.password" \
      --diff -vv'

Addons

The config-bearing Nextcloud apps are declared in meta/addons/ under the unified addon contract (requirement 026). Each declaration carries its full occ config:app:set payload under config:. The enable-only appstore apps stay under nextcloud.plugins in meta/services.yml.

Addon Mechanism Default state Bridges
sociallogin plugin enabled when the SSO OIDC plugin selector picks it sso → web-app-keycloak
user_ldap plugin enabled with the ldap service ldap → svc-db-openldap
bbb plugin enabled with the bigbluebutton partner bigbluebutton → web-app-bigbluebutton
onlyoffice plugin enabled with the onlyoffice partner onlyoffice → web-svc-onlyoffice
richdocuments plugin enabled with the collabora partner collabora → web-svc-collabora
spreed plugin enabled with the talk service talk, coturn
whiteboard plugin required (always installed) none (self-hosted backend)
xwiki plugin enabled with the xwiki partner xwiki → web-app-xwiki

The SSO (sociallogin) and LDAP (user_ldap) login surfaces are covered by the OIDC/LDAP Playwright specs (requirements 017/018).

MCP Server

Nextcloud serves a Model Context Protocol endpoint through the AppAPI proxy of the context_agent ExApp.

Property Value
Endpoint /mcp on the contextagentmcp adapter sidecar
Container-network URL http://contextagentmcp:8080/mcp
Transport Streamable HTTP
Exposure internal (the sidecar is reachable on the container network only; the hub's own MCP route is not published to clients)
Auth credentials.mcp_bearer, sent as Authorization: Bearer <bearer>
Identity the Nextcloud account the app password belongs to; every tool call the adapter forwards runs with that account's permissions
Implementation adapter (svc-ai-mcp-adapter in mcp_passthrough mode, fronting the context_agent ExApp)
Default state off; mcp.enabled turns on when web-app-hermes, web-app-openclaw or web-app-openwebui is deployed

Deployment

The context_agent and contextagentmcp services in meta/services.yml render only while mcp.enabled is true. The first runs ghcr.io/nextcloud/context_agent, listens on its internal port for AppAPI only, and shares credentials.context_agent_app_secret with the ExApp registration as APP_SECRET. The second is the adapter sidecar, built from the staged svc-ai-mcp-adapter context, read-only with every capability dropped, and it is the only MCP endpoint clients are given.

The sidecar reads its upstream credential from an upstream.env file rather than from the compose environment, because the app password does not exist when the stack is first rendered. tasks/utils/mcp.yml writes that file after the mint and recreates the sidecar through svc-ai-mcp-adapter's rebuild.yml: compose up resolves env_file while creating the container, which a restart would not.

tasks/utils/mcp.yml waits for the ExApp heartbeat, registers the deploy daemon and the ExApp (route ^/mcp, verbs POST,GET,DELETE, access level 1), enables the ExApp, mints an app password for the dedicated MCP account via occ user:auth-tokens:add, persists it through sys-token-store under users['mcp-web-app-nextcloud'].tokens['web-app-nextcloud'], and asserts that an authenticated initialize call answers 200.

Authorization subject

auth_subject: service_account: tasks/utils/mcp/token.yml creates the account NEXTCLOUD_MCP_USERNAME with occ user:add if it is missing, mints the app password against that account, and stores it under mcp.credential.owner. Every call carries that account's rights, not the administrator's, no matter who asked the client. Reaching the tool server is gated on the role's mcp RBAC group, which is a separate grant from holding a Nextcloud account.

Tool categories

The 2.7.0 image ships 23 tool categories, which are application names (calendar, files, mail, talk, …) rather than operations: each bundles its read and its write tools, and the hub offers no setting that removes only the mutating ones. Enabling the ExApp writes true for every category absent from tool_status, so the hub itself serves all of them, write tools included.

The adapter is what bounds that. files/mcp/tools.json names the nine read tools of calendar, contacts and files — the three categories whose upstream is_available is unconditional, so the set does not vary with which optional apps a deployment installs. svc-ai-mcp-adapter refuses any tool outside that list and any tool marked mutating while mutating_tools_enabled is false, and it refuses to start at all if the contract's schema_sha256 does not match the file. The refusal happens in the sidecar, so it holds for every client regardless of what each renders locally.

The hub keeps all its categories. Nextcloud's own Assistant is unaffected by the bound: it reads the same tool_status, and nothing here narrows it.

The app password never reaches a client. Clients present credentials.mcp_bearer to the sidecar; the sidecar presents the app password upstream. Revoking one does not revoke the other.

occ app_api:app:config:get context_agent tool_status
occ app_api:app:config:set context_agent tool_status --value '<json map of category to bool>'

Widening the exposed set means adding the tool to files/mcp/tools.json, recomputing both adapter.specification_sha256 and tools.schema_sha256, and listing it in mcp.tools.allowlist.

Verification

The sidecar is reachable on the container network only, so a client probes it from inside the stack:

curl -i -X POST \
  -H 'Authorization: Bearer <credentials.mcp_bearer>' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"cli","version":"1"}}}' \
  http://contextagentmcp:8080/mcp

From outside, every MCP path answers 404. The hub serves its route under two spellings — with and without the /index.php front controller — and roles/sys-svc-proxy/templates/mcp/vhost.conf.j2 withdraws both, so neither reaches the Context Agent. files/playwright/test-mcp-guest.js probes all three public paths and asserts each answers >= 300 without an MCP protocol body.

Default state

Off. mcp.enabled is true only while web-app-hermes, web-app-openclaw or web-app-openwebui is part of the deployment.

How to disable

Remove the MCP client roles, or pin mcp.enabled: false for this role. The context_agent service is then not rendered, the ExApp is not registered with AppAPI, and no app password is minted.

Documentation

A detailed documentation for the use and administration of Nextcloud on Infinito.Nexus you will find here.

Further Resources

Persona contract opt-outs

The shared biber and administrator persona helpers are declared blocked in templates/playwright.env.j2. Nextcloud presents three different login surfaces depending on the variant (native, oidc_login, sociallogin), the native administrator authenticates with the role-local credentials.administrator_password rather than the Keycloak secret, and the #firstrunwizard modal intercepts the user-menu click the generic logout depends on. Both personas are therefore driven by the role's own login specs — test-login-admin-native.js, test-login-admin-oidc.js, test-login-biber-oidc.js and test-login-biber-ldap.js under files/playwright — which carry the flavor switch, the modal dismissal and the login retry the shared helper lacks.

Credits

Implemented by Kevin Veen-Birkenbach. Part of the Infinito.Nexus Project and maintained by Kevin Veen-Birkenbach. Licensed under the Infinito.Nexus Community License (Non-Commercial).