Skip to content

Nextcloud AppAPI HaProxy Reversed Proxy (HaRP)


Overview

HaRP is a reverse proxy system designed to simplify the deployment workflow for Nextcloud 32’s AppAPI.

It enables direct communication between clients and ExApps, bypassing the Nextcloud instance to improve performance and reduce the complexity traditionally associated with DockerSocketProxy setups.

HaRP provides a flexible and scalable solution for managing ExApps, supporting deployments both locally and on remote servers.

It can be installed alongside Nextcloud or on a separate host, allowing for optimized performance and security.

The system supports simultaneous HTTP and HTTPS communication, enabling trusted networks to use direct HTTP access while securing external or untrusted connections via HTTPS.

In addition, HaRP includes built-in brute-force protection and dynamic routing capabilities, making it well-suited for a wide range of network infrastructures, from simple home setups to large distributed environments.


What Does HaRP Do?

  • Simplifies Deployment: Replaces more complex setups (such as DockerSocketProxy) with an easy-to-use container.
  • Direct Communication: Routes requests directly to ExApps, bypassing the Nextcloud instance.
  • Enhanced Security: Uses brute-force protection and basic authentication to secure all exposed interfaces.
  • Flexible Frontends: Supports both HTTP and HTTPS for ExApps and Nextcloud control, and FRP (TCP) frontend.
  • Multi-Docker Management: A single HaRP instance can manage multiple Docker engines.
  • Automated TLS for FRP: Generates self-signed certificates for FRP communications (unless explicitly disabled).

How to Install It

Deploying HaRP

HaRP should be deployed where your reverse proxy (NGINX, Caddy, Traefik, etc.) can reach its HP_EXAPPS_ADDRESS. For home installations, you may run it on your Nextcloud instance. Below are a couple of deployment examples using Docker:

Basic Docker Deployment

docker run \
  -e HP_SHARED_KEY="some_very_secure_password" \
  -e NC_INSTANCE_URL="http://nextcloud.local" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v `pwd`/certs:/certs \
  --name appapi-harp -h appapi-harp \
  --restart unless-stopped \
  -p 8780:8780 \
  -p 8782:8782 \
  -d ghcr.io/nextcloud/nextcloud-appapi-harp:release

Note: By default, HP_EXAPPS_ADDRESS is set to 0.0.0.0:8780 — ensure this port is published to the desired interface (for example, host’s 127.0.0.1:8780).

Using Host Networking

For even faster communication by avoiding internal network routing, you can use host networking:

docker run \
  -e HP_SHARED_KEY="some_very_secure_password" \
  -e NC_INSTANCE_URL="http://nextcloud.local" \
  -e HP_EXAPPS_ADDRESS="192.168.2.5:8780" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v `pwd`/certs:/certs \
  --name appapi-harp -h appapi-harp \
  --restart unless-stopped \
  --network host \
  -d ghcr.io/nextcloud/nextcloud-appapi-harp:release

Warning: Do not forget to change the HP_SHARED_KEY value to a secure one!

Advanced Docker Deployment with Nextcloud and Docker Host behind Apache Reverse Proxy

On the Docker Host
Creation of Cert folder (if necessary)

mkdir -p /some/path/certs

Open ports (based on Almalinux - RHEL Distros)
firewall-cmd --permanent --zone=public --add-port=8780/tcp
firewall-cmd --permanent --zone=public --add-port=8782/tcp
firewall-cmd --reload
Deploy of the HaRP Container
docker run \
  -e HP_SHARED_KEY="some_very_secure_password" \
  -e NC_INSTANCE_URL="https://cloud.acme.com" \
  -e HP_TRUSTED_PROXY_IPS="192.168.0.0/24" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /some/path/certs:/certs \
  -p 8780:8780 \
  -p 8782:8782 \
  --name appapi-harp -h appapi-harp \
  --restart unless-stopped \
  -d ghcr.io/nextcloud/nextcloud-appapi-harp:release

Note: You have to configure the NC_INSTANCE_URL value with your public Nextcloud url and the HP_TRUSTED_PROXY_IPS value with your local network (CDIR) that hosts your reverse proxy and your Nextcloud instance.


Configuring Your Reverse Proxy

HaRP requires your reverse proxy to forward traffic from your public domain (e.g., nextcloud.com/exapps/) to the HaRP container’s HP_EXAPPS_ADDRESS. Below are sample configurations for NGINX, Caddy, and Traefik:

NGINX Example

server {
    listen 80;
    server_name nextcloud.com;

    location /exapps/ {
        proxy_pass http://127.0.0.1:8780/exapps/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 1800s;
    }
}

The proxy_http_version and the Upgrade/Connection headers let WebSocket connections through to ExApps; nginx drops them otherwise.

If you point proxy_pass at a container or DNS name instead of an IP (for example appapi-harp on a user-defined Docker network), do not write the name into proxy_pass directly: nginx resolves it once at startup and refuses to start whenever that container is absent (host not found in upstream), which takes every site on that nginx down. Put the upstream in a variable, which nginx resolves per request, and give it a resolver. Use this block instead of the one above, not next to it:

server {
    listen 80;
    server_name nextcloud.com;

    resolver 127.0.0.11 valid=30s;   # Docker's embedded DNS; use your own resolver outside Docker
    # no /exapps/ suffix: with a variable, nginx would send every request to exactly that path
    set $harp_upstream http://appapi-harp:8780;

    location /exapps/ {
        proxy_pass $harp_upstream;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 1800s;
    }
}

127.0.0.11 only answers inside containers on a user-defined network (such as a Compose network), not on the default bridge network and not for nginx on the host. nginx's resolver also ignores /etc/hosts, so names added with --add-host or extra_hosts do not resolve this way; keep the plain proxy_pass form for those.

Caddy Example

nextcloud.com {
    reverse_proxy /exapps/* 127.0.0.1:8780 {
        transport http {
            read_timeout 1800s
        }
    }
}

Traefik Example

http:
  routers:
    exapps:
      rule: "PathPrefix(`/exapps/`)"
      service: exapps-service
      entryPoints:
        - web
  services:
    exapps-service:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:8780"
        serversTransport: exapps-transport
  serversTransports:
    exapps-transport:
      forwardingTimeouts:
        responseHeaderTimeout: 1800s

Note: Replace 127.0.0.1 with the actual IP address of your HaRP container if it is running on a different host.

Note: The 1800s (30 minutes) read timeout matches HaRP's default HP_TIMEOUT_SERVER value. This is required for slow-responding ExApps (e.g., context_chat_backend) that may take a long time to process requests like document indexing or AI responses.

Apache Example

On the Apache Reverse Proxy Host - Reverse proxy redirections

On the virtual Host "cloud.acme.com" of the apache conf file Add the following lines (before the existing configuration)

#  AppAPI Configuration
ProxyPass /exapps/ http://<IP_host2_docker>:8780/exapps/
ProxyPassReverse /exapps/ http://<IP_host2_docker>:8780/exapps/
ProxyTimeout 1800

Cloudflare Tunneling Example

cloudflare-tunnel-1

cloudflare-tunnel-2

Note: The order of the routes matters, move the exapp/* route above your Nextcloud's main route.


Nextcloud Configuration

Based on a infrastructure With 3 hosts :

  • Reverse Proxy
  • Nextcloud
  • Docker

On the Nextcloud Web Interface - Daemon Register

Add the following configuration :

Daemon Configuration template : HaRP Proxy (HOST)
Surname : appapi-harp
Display name : appapi-harp
Deployment method : docker-install
HaRP host : <IP_host2_docker>:8780
HaRP shared key : some_very_secure_password
Nextcloud URL : https://cloud.acme.com
FRP server address : <IP_host2_docker>:8782
Docker network : bridge

Finally, test the whole setup with “Test deploy” in the 3-dots menu of the deploy daemon.

Additional tests from the network of your hosts (based on Almalinux - RHEL Distros)

curl -fsS \
  -H "harp-shared-key: some_very_secure_password" \
  -H "docker-engine-port: 24000" \
  http://<IP_host2_docker>:8780/exapps/app_api/v1.41/_ping
curl -fsS \
  -H "harp-shared-key: some_very_secure_password" \
  -H "docker-engine-port: 24000" \
  https://cloud.acme.com/exapps/app_api/v1.41/_ping

Environment Variables

HaRP is configured via several environment variables. Here are the key variables and their defaults:

  • HP_EXAPPS_ADDRESS / HP_EXAPPS_HTTPS_ADDRESS

    • Description: IP:Port for ExApps HTTP/HTTPS frontends.
    • Default:
      • HP_EXAPPS_ADDRESS="0.0.0.0:8780"
      • HP_EXAPPS_HTTPS_ADDRESS="0.0.0.0:8781"
    • Note: Must be reachable by your reverse proxy.
  • HP_TRUSTED_PROXY_IPS

    • Description: A comma-separated list of trusted reverse proxy IP addresses or CIDR ranges. When HaRP is behind another reverse proxy (like NGINX), set this to the IP of that proxy to allow HaRP to correctly identify the true client IP from X-Forwarded-For or X-Real-IP headers.
    • Default: "" (disabled)
    • Example: "172.18.0.0/16,127.0.0.1"
    • Note: Quote characters that end up inside the value (this happens with --env-file files and compose environment: list entries) are stripped automatically. A range with host bits set, e.g. 192.168.100.20/24, is interpreted as its network (192.168.100.0/24), the same way Nextcloud handles trusted_proxies. Entries that still fail to parse are ignored individually and logged.
  • HP_FRP_ADDRESS

    • Description: IP:Port for the FRP (TCP) frontends.
    • Default: HP_FRP_ADDRESS="0.0.0.0:8782"
    • Note: Should be accessible from where your ExApps are running.
  • HP_SPOA_ADDRESS

    • Description: IP:Port for the internal SPOE agent that HAProxy uses for request authentication.
    • Default: HP_SPOA_ADDRESS="127.0.0.1:9600"
    • Note: Only change if port 9600 conflicts with another service.
  • HP_SHARED_KEY (or HP_SHARED_KEY_FILE)

    • Description: A secret token used for authentication between services.
    • Requirement: Must be set at runtime. Use only one of these methods.
    • Important: Must contain only ASCII characters (a-z, A-Z, 0-9, and common symbols).
  • NC_INSTANCE_URL

    • Description: The base URL of your Nextcloud instance.
    • Requirement: Must be accessible from the HaRP container.
  • HP_FRP_DISABLE_TLS

    • Description: Disables TLS for the FRP service.
    • Default: HP_FRP_DISABLE_TLS="false"
    • Advanced: Use only for specialized setups where TCP TLS termination is managed externally.
  • HP_LOG_LEVEL

    • Default: warning
    • Possible Values: debug, info, warning, error
  • HP_WATCHDOG_ENABLED / HP_WATCHDOG_INTERVAL / HP_WATCHDOG_FAILS

    • Description: Self-healing watchdog. Every HP_WATCHDOG_INTERVAL seconds the container probes the internal agent (GET /heartbeat); after HP_WATCHDOG_FAILS consecutive failures the agent is killed and the container exits so that the Docker restart policy (--restart unless-stopped in the examples above) brings it back in a clean state. The death of any core process (agent, frps, frpc, HAProxy) also stops the container now instead of leaving it running half-broken.
    • Default: HP_WATCHDOG_ENABLED="true", HP_WATCHDOG_INTERVAL="10", HP_WATCHDOG_FAILS="12". Each failed probe can additionally spend the probe's own 5s timeout, so with the defaults a dead agent is detected in about 2 minutes and a hung-but-connectable one in about 3 minutes.
    • Reloading HAProxy: sending SIGHUP reloads the HAProxy configuration and certificates without restarting the container (same as before): docker exec appapi-harp kill -HUP 1. Prefer this over docker kill -s HUP: after any docker kill the Docker daemon treats the container as manually stopped and will not auto-restart it on its next exit until it is started manually again.
  • HP_VERBOSE_START

    • Description: Flag that determines whether to output verbose logging to the console during container startup.
    • Default: 1
  • HP_SESSION_LIFETIME

    • Description: A floating-point value that determines how long the Nextcloud session is retained in HaRP, in seconds. Possible values range from 0 (disable session caching) to 10 seconds.
    • Default: 3
  • HP_BLACKLIST_COUNT

    • Description: The maximum no. of bad status codes (4xx, 5xx) before the IP is banned for HP_BLACKLIST_WINDOW seconds.
    • Default: 10
  • Timeout Variables:

    • HP_TIMEOUT_CONNECT
      • Description: Maximum time allowed for establishing a connection.
      • Default: 30s
    • HP_TIMEOUT_CLIENT
      • Description: Inactivity timeout for the request phase, while HaRP waits for the client to send its request. It does not apply to established streaming responses (SSE), which are governed by HP_TIMEOUT_SERVER. We do not recommend to change this value, as raising it weakens protection against slow clients.
      • Default: 30s
    • HP_TIMEOUT_SERVER
      • Description: Timeout for server-side connections. Also caps how long an established streaming response (for example an SSE stream) may stay idle. We do not recommend to change this value.
      • Default: 1800s
    • HP_TIMEOUT_TUNNEL
      • Description: Inactivity timeout for upgraded connections (WebSockets). A value of 0 is treated as unlimited (mapped to HAProxy's 24d maximum), at the cost of vanished peers holding connections open for a very long time. Unlimited applies only while the connection stays open in both directions: half-closed connections are still reaped after 30 seconds.
      • Default: the value of HP_TIMEOUT_SERVER (1800s)
    • HP_BLACKLIST_WINDOW
      • Description: Timeout after which an IP is removed from the blacklist, in seconds.
      • Default: 300

Connecting Docker Engines

HaRP supports two approaches for connecting Docker Engines:

1. Direct Mounting (Local Docker Engine)

If your Docker Engine is running on the same host as HaRP, simply mount the Docker socket into the container. This direct method allows HaRP to interact with the Docker Engine immediately:

-v /var/run/docker.sock:/var/run/docker.sock

2. Connecting External Docker Engines via FRP

For remote or external Docker Engines - or if you prefer not to mount the Docker socket - you can use an FRP (Fast Reverse Proxy) client to establish a secure connection. Follow these steps:

  1. Retrieve Certificate Files: HaRP automatically generates the necessary FRP certificate files, and places them in its folder /certs/frp. You need next files from it to connect external Docker Engine to HaRP:

    • client.crt
    • client.key
    • ca.crt
    mkdir -p harp_frpc_docker/certs/frp
    cd harp_frpc_docker
    for f in {client.crt,client.key,ca.crt}; do docker cp appapi-harp:/certs/frp/$f certs/frp/; done
  2. Create an FRP Client Configuration: With the certificate files in hand, create a configuration file (for example, frpc.toml) on the Docker Engine host in the "harp_frpc_docker" folder. Below is a sample configuration:

    # frpc.toml
    serverAddr = "your.harp.server.address"          # Replace with your HP_FRP_ADDRESS host
    serverPort = 8782                                # Default port for FRP or the port your reverse proxy listens on
    loginFailExit = false                            # If the FRP (HaRP) server is unavailable, continue trying to log in.
    
    transport.tls.certFile = "certs/frp/client.crt"
    transport.tls.keyFile = "certs/frp/client.key"
    transport.tls.trustedCaFile = "certs/frp/ca.crt"
    transport.tls.serverName = "harp.nc"             # DO NOT CHANGE THIS VALUE
    
    metadatas.token = "HP_SHARED_KEY"                # HP_SHARED_KEY in quotes
    
    [[proxies]]
    remotePort = 24001                               # Unique remotePort for each Docker Engine (range: 24001-24099)
    name = "deploy-daemon-1"                         # Unique name for each Docker Engine
    type = "tcp"
    [proxies.plugin]
    type = "unix_domain_socket"
    unixPath = "/var/run/docker.sock"
  3. Deploy the FRP Client: Run the FRP client on the host with the Docker Engine using the configuration file. This establishes a secure tunnel between the remote Docker Engine and HaRP. Each connection requires a unique remotePort value; HaRP supports up to 99 Docker Engines by assigning a different port in the allowed range.

    Run like this:

     frpc -c /path/to/frpc.toml

    Or with docker:

    docker run \
      -v /path/to/frpc.toml:/etc/frpc.toml \
      -v `pwd`/certs:/certs \
      -v /var/run/docker.sock:/var/run/docker.sock \
      --restart unless-stopped \
      -d --name harp_frpc_docker \
      -- ghcr.io/fatedier/frpc:v0.61.1 "-c=/etc/frpc.toml"

    Or with docker compose:

    services:
      harp_frpc_docker:
        image: ghcr.io/fatedier/frpc:v0.61.1
        container_name: harp_frpc_docker
        volumes:
          - ./frpc.toml:/etc/frpc.toml
          - ./certs:/certs
          - /var/run/docker.sock:/var/run/docker.sock
        restart: unless-stopped
        command: -c=/etc/frpc.toml
    docker compose up -d

The FRP client-server connections, i.e. the connection from the above FRP client to the FRP server in the HaRP container, can be passed through the same reverse proxy as the Nextcloud instance for better security. The following is an example of how to configure NGINX for this purpose:

  stream {
      server {
          listen 8782;  # Replace with the port you want to listen on
          proxy_pass 127.0.0.1:8782;
          proxy_protocol off;
          proxy_connect_timeout 10s;
          proxy_timeout 300s;
      }
  }

Note: These FRP certificates are valid for HP_FRP_CERT_VALIDITY_DAYS days (default 5000, ~13 years) and are not renewed automatically. The FRP connection uses mutual TLS, so once a certificate expires the tunnel stops working. To renew them, stop HaRP, delete its /certs/frp folder, and start HaRP again, then re-copy client.crt, client.key, and ca.crt to each external Docker Engine and restart its frpc.

ExApps deployed through HaRP embed these certificates at install time, so after regenerating them you must remove and re-install each ExApp for it to pick up the new certificates (a restart is not enough).

Kubernetes Backend

Besides Docker, HaRP can deploy ExApps directly to a Kubernetes cluster. Set HP_K8S_ENABLED=true and HaRP manages the ExApps itself through the Kubernetes API instead of a Docker Engine, creating one Deployment (one per role for multi-role ExApps) and one shared PersistentVolumeClaim per ExApp inside a single namespace, plus one Service for the nodeport, clusterip and loadbalancer exposure types (manual exposure stores the upstream address as annotations on the Deployment instead).

The namespace and storage behaviour are controlled by these variables:

  • HP_K8S_ENABLED
    • Description: Enables the Kubernetes backend. HaRP then talks to the Kubernetes API instead of a Docker Engine.
    • Default: false
  • HP_K8S_NAMESPACE
    • Description: The namespace in which HaRP creates and manages the ExApp resources. HaRP never creates this namespace itself.
    • Default: nextcloud-exapps
  • HP_K8S_STORAGE_CLASS
    • Description: Storage class for the ExApp persistent volume claims. Leave empty to use the cluster default.
    • Default: empty
  • HP_K8S_DEFAULT_STORAGE_SIZE
    • Description: Default size of the persistent volume claim that HaRP creates for each ExApp.
    • Default: 10Gi
  • HP_K8S_HOST_ALIASES
    • Description: Additional host aliases set on the ExApp pods, as a comma-separated list of hostname:ip pairs, e.g. nextcloud.example.com:10.0.0.5. Useful when your Nextcloud domain is not resolvable by the cluster DNS.
    • Default: empty

How HaRP reaches the API server:

  • HP_K8S_API_SERVER
    • Description: URL of the Kubernetes API server. Use an https:// URL: HaRP sends its bearer token with every request, and it does not reject other schemes, so any other scheme transmits the token in clear text.
    • Default: derived from the in-cluster environment (https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT) when HaRP runs as a pod, otherwise empty
  • HP_K8S_BEARER_TOKEN / HP_K8S_BEARER_TOKEN_FILE
    • Description: The service account token, as a value or as a file path. Set only one of them. The value is read once at startup and never refreshed; the file is re-read on every request, so a token that is rotated in place is picked up.
    • Default: the token mounted into the pod, /var/run/secrets/kubernetes.io/serviceaccount/token
  • HP_K8S_CA_FILE
    • Description: CA certificate used to verify the API server. Needed outside the cluster when the API server uses a private CA. If the file does not exist, HaRP falls back to the system trust store.
    • Default: the CA mounted into the pod, /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
  • HP_K8S_VERIFY_SSL
    • Description: Set to false to skip verification of the API server certificate. Only for test clusters.
    • Default: true

Inside the cluster nothing has to be set beyond HP_K8S_ENABLED: HaRP authenticates with the service account mounted into its pod. Outside the cluster, point HP_K8S_API_SERVER at the API server and supply the token of a service account that carries the permissions below. Give that token a lifetime that outlives HaRP: kubectl create token issues a one hour token by default, so pass --duration (the development scripts use 8760h) or bind a kubernetes.io/service-account-token Secret to the account for a token that does not expire. The /info endpoint reports whether the API server answers; that probe reads the discovery endpoint /api, which every authenticated identity may read by default, so it needs no permission of its own. It proves connectivity and authentication only: with a wrong RoleBinding the API server is still reported as reachable, and the missing permission surfaces as a 403 on the first install.

RBAC Permissions

HaRP uses the Kubernetes API only to manage ExApp workloads, and only inside HP_K8S_NAMESPACE. The following is the complete set of permissions it requires. Nothing wider is needed, and a namespaced Role is enough for every exposure type except NodePort (see below).

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: harp-exapps
  namespace: nextcloud-exapps   # the ExApp namespace, must match HP_K8S_NAMESPACE
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "create", "patch", "delete"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["get", "list", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["create", "delete"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: harp-exapps
  namespace: nextcloud-exapps   # the ExApp namespace, where the Role lives
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: harp-exapps
subjects:
  - kind: ServiceAccount
    name: harp                  # the service account of the HaRP pod, or the one whose token HaRP uses
    namespace: nextcloud        # the namespace the HaRP pod runs in, not the ExApp namespace

The Role and the RoleBinding live in the ExApp namespace, because that is where the permissions apply. The subject is the service account HaRP authenticates with, which usually lives in another namespace (the one the HaRP pod runs in, or wherever the token for an out-of-cluster HaRP was issued). If the subject's name or namespace do not match that account exactly, every request HaRP makes answers 403. The manifest creates neither the namespace nor the service account: create them first (kubectl create namespace nextcloud-exapps, kubectl -n nextcloud create serviceaccount harp) and run the HaRP pod with serviceAccountName: harp.

What each permission is used for:

Resource Verbs Used for
apps/deployments create Creating the ExApp Deployment when an ExApp is installed.
apps/deployments get, list Checking whether an ExApp already exists before it is installed, and reading the upstream address of a manual ExApp back from the Deployment annotations when the ExApp is enabled and after a HaRP restart.
apps/deployments patch Scaling an ExApp to 1 replica when it is enabled and to 0 when it is disabled, and recording the exposure details on the Deployment. Strategic merge patches only.
apps/deployments delete Removing an ExApp.
services create, get, list, delete Exposing an ExApp, reading the assigned port or address back when the ExApp is enabled and after a HaRP restart, waiting for a LoadBalancer address to be assigned, and cleaning up on removal. manual exposure creates no Service, but get and list are still used: HaRP looks for a Service before it reads the Deployment annotations, and checks for one on removal.
persistentvolumeclaims create, delete The volume that backs an ExApp, sized via HP_K8S_DEFAULT_STORAGE_SIZE and placed via HP_K8S_STORAGE_CLASS.
pods list Polling readiness while an ExApp starts, so that image pull failures are reported at once instead of after the startup timeout.

HaRP does not need, and never requests, any of: secrets, configmaps, events, namespaces, pods/log, pods/exec, watch on any resource, or update on any resource. It does not create the namespace, and it sets no ownerReferences, so nothing is garbage-collected implicitly.

NodePort Also Needs nodes

nodes is cluster-scoped and therefore cannot be granted by a namespaced Role. HaRP reads it whenever an ExApp is exposed as NodePort (occ app_api:daemon:register ... --k8s_expose_type nodeport), because it has to pick a node address to route to. It does so when the ExApp is exposed, again when AppAPI enables the ExApp, and after every HaRP restart, when the upstream address is resolved from the Service anew. A fixed --k8s_upstream_host only skips the lookup during the expose call itself, so the permission is still needed:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: harp-nodes
rules:
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: harp-nodes
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: harp-nodes
subjects:
  - kind: ServiceAccount
    name: harp                  # the same service account as in the RoleBinding above
    namespace: nextcloud

With clusterip (the default of occ app_api:daemon:register --k8s_expose_type), loadbalancer or manual exposure HaRP never reads nodes, and the namespaced Role above is sufficient on its own, with no cluster-scoped permissions at all.

Can Write Access Be Limited to Deployment Time?

No. Kubernetes RBAC is static, so there is no time-boxed or just-in-time grant. The only way to approximate it would be to add and remove the RoleBinding around each operation.

It would also not achieve much, because writes are not confined to ExApp installation. Enabling an ExApp patches its Deployment to 1 replica, disabling it patches it back to 0, and removing it deletes the Deployment, the Service HaRP created and, when the data is removed too, the PersistentVolumeClaim. These are routine administrator actions in the Nextcloud UI rather than one-off deployment steps. If the service account only had get and list at that moment, the Kubernetes API would answer 403 and HaRP would surface the failure to the administrator.

The reduction that does work is scope rather than time, and the Role above already applies it: a single namespace, four resource types, no watch, no update, no access to secrets or configmaps, and no cluster-scoped permission at all unless NodePort is in use.

Hardening the ExApp Pods

HaRP does not set serviceAccountName on the ExApp pods, so they run under the default service account of HP_K8S_NAMESPACE. Leave that service account without any RoleBinding, which is how it starts out, and consider setting automountServiceAccountToken: false on it so that ExApp containers receive no Kubernetes API credentials at all.

HaRP sets no imagePullSecrets on the ExApp pods either. If the ExApp images come from a registry that needs authentication, add the pull secret to that default service account (kubectl -n <namespace> patch serviceaccount default -p '{"imagePullSecrets":[{"name":"<secret>"}]}'); Kubernetes then attaches it to every pod that runs under that service account, which is all ExApp pods. ExApp containers use imagePullPolicy: IfNotPresent, or Never when AppAPI maps the image registry to local, in which case the image has to be present on every node that can run the pod.

Adapting ExApps to use HaRP

We strongly recommend starting support for HaRP in ExApps from the start of Nextcloud 32, as the old DSP way will be deprecated and marked for removal in Nextcloud 35.

Adding HaRP support is fully compatible with the existing DSP system, so you won’t need to maintain two separate release types of your ExApp.

  1. Copy the start.sh script from the exapps_dev folder of the HaRP repository into your Docker image (e.g., using a COPY instruction).

  2. In your ExApp's Dockerfile, set the ENTRYPOINT to execute start.sh followed by the command and arguments required to launch your actual application. The start.sh script will perform launch of FRP client if needed and then use exec to run the command you provide as arguments.

  3. Ensure the curl command-line utility is installed in your ExApp's Docker image, as it's needed by the following script to download the FRP client.

  4. Add the following lines to your Dockerfile to automatically include the FRP client binaries in your Docker image:

    # Download and install FRP client with checksum verification
    # FRP version and checksums - update these when upgrading
    ARG FRP_VERSION=0.61.1
    ARG FRP_AMD64_SHA256=bff260b68ca7b1461182a46c4f34e9709ba32764eed30a15dd94ac97f50a2c40
    ARG FRP_ARM64_SHA256=af6366f2b43920ebfe6235dba6060770399ed1fb18601e5818552bd46a7621f8
    
    RUN set -ex; \
        ARCH=$(uname -m); \
        if [ "$ARCH" = "aarch64" ]; then \
            FRP_ARCH="arm64"; \
            FRP_SHA256="${FRP_ARM64_SHA256}"; \
        else \
            FRP_ARCH="amd64"; \
            FRP_SHA256="${FRP_AMD64_SHA256}"; \
        fi; \
        FRP_URL="https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${FRP_ARCH}.tar.gz"; \
        echo "Downloading FRP v${FRP_VERSION} for ${FRP_ARCH}..."; \
        curl -fsSL "${FRP_URL}" -o /tmp/frp.tar.gz; \
        ACTUAL_SHA256=$(sha256sum /tmp/frp.tar.gz | cut -d' ' -f1); \
        if [ "$ACTUAL_SHA256" != "$FRP_SHA256" ]; then \
            echo "Checksum verification failed for FRP v${FRP_VERSION} (${FRP_ARCH})"; \
            echo "Expected: ${FRP_SHA256}"; \
            echo "Got:      ${ACTUAL_SHA256}"; \
            exit 1; \
        fi; \
        tar -C /tmp -xzf /tmp/frp.tar.gz; \
        cp /tmp/frp_${FRP_VERSION}_linux_${FRP_ARCH}/frpc /usr/local/bin/frpc; \
        chmod +x /usr/local/bin/frpc; \
        rm -rf /tmp/frp_${FRP_VERSION}_linux_${FRP_ARCH} /tmp/frp.tar.gz; \
        echo "FRP client installed successfully"

    Note: The checksums are verified against the official FRP releases to prevent supply chain attacks. When upgrading FRP, update both the version and checksums from the FRP releases page.

    Note: For Alpine 3.21 Linux you can just install FRP from repo using apk add frp command.

That's it! Your ExApp is now adapted to Nextcloud 32.

Nextcloud 32: Migrating Existing ExApps from DSP to HaRP

Note: All ExApps developed by Nextcloud will support HaRP when Nextcloud 32 is released. We hope that most ExApps from the community will also support it. Contact us if you need assistance.

If you've upgraded to Nextcloud 32 and want to switch from using DSP to HaRP, follow these steps:

  1. Install HaRP on the same Docker Engine that you were using for DSP.
  2. Test Deployment on HaRP with usual TestDeploy button.
  3. Set HaRP as the default deployment daemon for ExApps.
  4. Remove the ExApps without deleting their data volumes:
    • Terminal: Do not use the --rm-data option when removing the app.
    • From UI: Do not use the "Delete data when removing" checkbox.
  5. Install the ExApp: Install removed ExApps, now they will be installed on HaRP.
  6. Remove DSP: Now DSP (Docker Socket Proxy) can be safely removed.

Building & Deploying HaRP from Source

This section provides helper information for developing and modifying HaRP. The nextcloud-docker-dev environment will be used for this purpose.

Remove Any Existing HaRP Container

docker container remove --force appapi-harp

Build a Local HaRP Image from Source

docker build -t nextcloud-appapi-harp:local .

Deploy HaRP Using the Locally Built Image

docker run \
  -e HP_SHARED_KEY="some_very_secure_password" \
  -e NC_INSTANCE_URL="http://nextcloud.local" \
  -e HP_LOG_LEVEL="debug" \
  -e HP_VERBOSE_START="1" \
  -e HP_TRUSTED_PROXY_IPS="192.168.0.0/16" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v `pwd`/certs:/certs \
  --name appapi-harp -h appapi-harp \
  --restart unless-stopped \
  --network=master_default \
  -p 8780:8780 \
  -p 8782:8782 \
  -d nextcloud-appapi-harp:local

Important

Be mindful of checking and changing the environment variables HP_SHARED_KEY, NC_INSTANCE_URL, and HP_TRUSTED_PROXY_IPS in the above command to suit your environment and setup.

Debugging HaRP

One time initializing steps:
  1. Create virtual environment

  2. Install pydantic (you can look at exact version in the *Dockerfile) and git+https://github.com/cloud-py-api/haproxy-python-spoa.git

  3. Set next environment variables for running haproxy_agent.py script:

    HP_LOG_LEVEL=info;NC_INSTANCE_URL=http://nextcloud.local;HP_SHARED_KEY=some_very_secure_password;HP_FRP_DISABLE_TLS=true
    
  4. Create folder dev at the root of repository, extract there content of the desired archive with the FRP archive which is located at exapps_dev folder of this repo.

  5. Edit the data/nginx/vhost.d/nextcloud.local_location file from the nextcloud-docker-dev to point /exapps/ web route to the host:

    proxy_pass http://172.17.0.1:8780/exapps/;
    

    Note: my original content from my dev machine of file nextcloud.local_location:

    location /exapps/ {
      proxy_pass http://172.17.0.1:8780/exapps/;
    }
  6. Use docker compose up -d --force-recreate proxy command from Julius nextcloud-docker-dev to recreate the proxy container.

  7. Register HaRP from the Host template. Replace localhost with host.docker.internal in HaRP Host field.

Steps to run all parts of HaRP after initializing:
  1. Run FRP Server with ./dev/frps -c ./development/debugging/frps.toml command.
  2. Run the FRP Client to connect Docker Engine to the FRP Server with ./dev/frpc -c ./development/debugging/frpc.toml command.
  3. Run ./development/debugging/redeploy_haproxy_host.sh command to redeploy appapi-harp container with HaProxy only.

Note: Existing appapi-harp container will be removed.

Troubleshooting

Verify that HaRP can reach the Docker Engine

Use the Docker Engine /_ping endpoint via HaRP’s ExApps HTTP frontend to confirm connectivity:

curl -fsS \
  -H "harp-shared-key: <HP_SHARED_KEY>" \
  -H "docker-engine-port: 24000" \
  http://127.0.0.1:8780/exapps/app_api/v1.44/_ping
  • 24000 is the default FRP remote port used by the HaRP container for the built‑in/local Docker Engine (enabled when /var/run/docker.sock is mounted).
  • If you have connected additional Docker Engines via FRP, replace 24000 with the corresponding remotePort you configured (typically 24001–24099).
  • A response body of OK means the Docker Engine API is reachable from HaRP.

Common outcomes

  • OK – Success: HaRP can reach the Docker Engine on the given port.
  • 401 Unauthorized – The harp-shared-key header does not match HP_SHARED_KEY.
  • 503 Service Unavailable / 504 Gateway Timeout – Wrong docker-engine-port, FRP tunnel is down, or the Docker Engine is not reachable.
  • Connection errors – The address in HP_EXAPPS_ADDRESS (port 8780 by default) is not reachable from where you ran curl.

Note: If you expose the ExApps frontend over HTTPS (via HP_EXAPPS_HTTPS_ADDRESS and a mounted /certs/cert.pem), use https://...:8781 instead of http://...:8780.

Verify Docker Engines from inside the HaRP container

These checks run inside the HaRP container (e.g., docker exec -it appapi-harp sh).

1) Local Docker Engine (mapped socket)

Confirm the host’s Docker socket is correctly mounted and reachable:

# Directly against the mounted UNIX socket
curl -fsS --unix-socket /var/run/docker.sock http://localhost/_ping
# Expected: OK

You can also verify the FRP tunnel that HaRP exposes for the bundled local engine (port 24000):

curl -fsS http://127.0.0.1:24000/_ping
# Expected: OK

2) Remote Docker Engine over FRP

For each external Docker Engine you connected via FRP (each with a unique remotePort, typically 24001–24099), test the TCP port that FRP exposes on the HaRP container:

# Replace 24001 with the remotePort you configured in that engine's frpc.toml
curl -fsS http://127.0.0.1:24001/_ping
# Expected: OK

3) Test full HAProxy routing (same path as AppAPI)

This tests the complete request flow through HAProxy, SPOE agent, and to the Docker engine — the same path that AppAPI uses when performing "Test deploy":

curl -v http://127.0.0.1:8780/exapps/app_api/_ping \
  -H "harp-shared-key: $HP_SHARED_KEY" \
  -H "docker-engine-port: 24000"
# Expected: OK

Note: Do NOT test against port 8200 directly (e.g., curl http://127.0.0.1:8200/_ping). Port 8200 is the internal control API and does not handle Docker API requests.

Verify that the Reverse Proxy configuration is correct

Stop the HaRP container temporarily and start a dummy server with nc (netcat) to confirm that your reverse proxy is forwarding requests correctly with all the headers:

# Stop HaRP container
docker stop appapi-harp
# Start a dummy server on the same port as HaRP's ExApps frontend
nc -l -k -p 8780

Now, send a request through your reverse proxy to the ExApps endpoint (e.g., https://cloud.example.com/exapps/hello):

curl -v https://cloud.example.com/exapps/hello

For nginx, you should see output similar to this in the terminal where nc is running:

GET /exapps/hello HTTP/1.1
Host: cloud.example.com
Connection: close
X-Real-IP: 20.207.73.82
X-Forwarded-For: 20.207.73.82 192.168.21.1
X-Forwarded-Host: cloud.example.com
X-Forwarded-Proto: https
X-Forwarded-Ssl: on
X-Forwarded-Port: 443
X-Original-URI: /exapps/hello
X-Forwarded-Proto: https
user-agent: curl/8.11.1
accept: */*

Contributing

Contributions to HaRP are welcome. Feel free to open issues, discussions or submit pull requests with improvements, bug fixes, or new features.

About

Fast Proxy for AppAPI(Nextcloud 32+)

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

55 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages