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.
- 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).
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:
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:releaseNote: By default,
HP_EXAPPS_ADDRESSis set to0.0.0.0:8780— ensure this port is published to the desired interface (for example, host’s 127.0.0.1:8780).
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:releaseWarning: Do not forget to change the HP_SHARED_KEY value to a secure one!
mkdir -p /some/path/certs
firewall-cmd --permanent --zone=public --add-port=8780/tcp
firewall-cmd --permanent --zone=public --add-port=8782/tcp
firewall-cmd --reload
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.
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:
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.
nextcloud.com {
reverse_proxy /exapps/* 127.0.0.1:8780 {
transport http {
read_timeout 1800s
}
}
}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: 1800sNote: Replace
127.0.0.1with 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 defaultHP_TIMEOUT_SERVERvalue. 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.
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 1800Note: The order of the routes matters, move the
exapp/*route above your Nextcloud's main route.
Based on a infrastructure With 3 hosts :
- Reverse Proxy
- Nextcloud
- Docker
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.
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
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-FororX-Real-IPheaders. - 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-filefiles and composeenvironment: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 handlestrusted_proxies. Entries that still fail to parse are ignored individually and logged.
- 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
-
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(orHP_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
- Default:
-
HP_WATCHDOG_ENABLED/HP_WATCHDOG_INTERVAL/HP_WATCHDOG_FAILS- Description: Self-healing watchdog. Every
HP_WATCHDOG_INTERVALseconds the container probes the internal agent (GET /heartbeat); afterHP_WATCHDOG_FAILSconsecutive failures the agent is killed and the container exits so that the Docker restart policy (--restart unless-stoppedin 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
SIGHUPreloads the HAProxy configuration and certificates without restarting the container (same as before):docker exec appapi-harp kill -HUP 1. Prefer this overdocker kill -s HUP: after anydocker killthe Docker daemon treats the container as manually stopped and will not auto-restart it on its next exit until it is started manually again.
- Description: Self-healing watchdog. Every
-
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) to10seconds. - Default:
3
- Description: A floating-point value that determines how long the Nextcloud session is retained in HaRP, in seconds. Possible values range from
-
HP_BLACKLIST_COUNT- Description: The maximum no. of bad status codes (4xx, 5xx) before the IP is banned for
HP_BLACKLIST_WINDOWseconds. - Default:
10
- Description: The maximum no. of bad status codes (4xx, 5xx) before the IP is banned for
-
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
- 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- 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
0is treated as unlimited (mapped to HAProxy's24dmaximum), 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)
- Description: Inactivity timeout for upgraded connections (WebSockets). A value of
HP_BLACKLIST_WINDOW- Description: Timeout after which an IP is removed from the blacklist, in seconds.
- Default:
300
HaRP supports two approaches for connecting Docker Engines:
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.sockFor 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:
-
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.crtclient.keyca.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
-
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"
-
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
remotePortvalue; 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_DAYSdays (default5000, ~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/frpfolder, and start HaRP again, then re-copyclient.crt,client.key, andca.crtto each external Docker Engine and restart itsfrpc.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).
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:ippairs, e.g.nextcloud.example.com:10.0.0.5. Useful when your Nextcloud domain is not resolvable by the cluster DNS. - Default: empty
- Description: Additional host aliases set on the ExApp pods, as a comma-separated list of
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
- Description: URL of the Kubernetes API server. Use an
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
falseto skip verification of the API server certificate. Only for test clusters. - Default:
true
- Description: Set to
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.
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 namespaceThe 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.
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: nextcloudWith 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.
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.
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.
We strongly recommend starting support for
HaRPin ExApps from the start of Nextcloud32, as the oldDSPway will be deprecated and marked for removal in Nextcloud35.Adding
HaRPsupport is fully compatible with the existingDSPsystem, so you won’t need to maintain two separate release types of your ExApp.
-
Copy the
start.shscript from theexapps_devfolder of the HaRP repository into your Docker image (e.g., using aCOPYinstruction). -
In your ExApp's
Dockerfile, set theENTRYPOINTto executestart.shfollowed by the command and arguments required to launch your actual application. Thestart.shscript will perform launch ofFRPclient if needed and then useexecto run the command you provide as arguments. -
Ensure the
curlcommand-line utility is installed in your ExApp's Docker image, as it's needed by the following script to download the FRP client. -
Add the following lines to your
Dockerfileto automatically include theFRP clientbinaries 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.21Linux you can just installFRPfrom repo usingapk add frpcommand.
That's it! Your ExApp is now adapted to Nextcloud 32.
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:
- Install HaRP on the same Docker Engine that you were using for DSP.
- Test Deployment on HaRP with usual
TestDeploybutton. - Set HaRP as the default deployment daemon for ExApps.
- Remove the ExApps without deleting their data volumes:
- Terminal: Do not use the
--rm-dataoption when removing the app. - From UI: Do not use the "Delete data when removing" checkbox.
- Terminal: Do not use the
- Install the ExApp: Install removed ExApps, now they will be installed on
HaRP. - Remove DSP: Now DSP (Docker Socket Proxy) can be safely removed.
This section provides helper information for developing and modifying HaRP. The nextcloud-docker-dev environment will be used for this purpose.
docker container remove --force appapi-harpdocker build -t nextcloud-appapi-harp:local .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:localImportant
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.
-
Create virtual environment
-
Install
pydantic(you can look at exact version in the *Dockerfile) andgit+https://github.com/cloud-py-api/haproxy-python-spoa.git -
Set next environment variables for running
haproxy_agent.pyscript:HP_LOG_LEVEL=info;NC_INSTANCE_URL=http://nextcloud.local;HP_SHARED_KEY=some_very_secure_password;HP_FRP_DISABLE_TLS=true -
Create folder
devat the root of repository, extract there content of the desired archive with the FRP archive which is located atexapps_devfolder of this repo. -
Edit the
data/nginx/vhost.d/nextcloud.local_locationfile from thenextcloud-docker-devto 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/; }
-
Use
docker compose up -d --force-recreate proxycommand from Juliusnextcloud-docker-devto recreate the proxy container. -
Register
HaRPfrom the Host template. Replacelocalhostwithhost.docker.internalinHaRP Hostfield.
- Run FRP Server with
./dev/frps -c ./development/debugging/frps.tomlcommand. - Run the FRP Client to connect Docker Engine to the FRP Server with
./dev/frpc -c ./development/debugging/frpc.tomlcommand. - Run
./development/debugging/redeploy_haproxy_host.shcommand to redeployappapi-harpcontainer with HaProxy only.
Note: Existing
appapi-harpcontainer will be removed.
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
24000is the default FRP remote port used by the HaRP container for the built‑in/local Docker Engine (enabled when/var/run/docker.sockis mounted).- If you have connected additional Docker Engines via FRP, replace
24000with the correspondingremotePortyou configured (typically24001–24099). - A response body of
OKmeans the Docker Engine API is reachable from HaRP.
Common outcomes
OK– Success: HaRP can reach the Docker Engine on the given port.401 Unauthorized– Theharp-shared-keyheader does not matchHP_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(port8780by default) is not reachable from where you ran curl.
Note: If you expose the ExApps frontend over HTTPS (via
HP_EXAPPS_HTTPS_ADDRESSand a mounted/certs/cert.pem), usehttps://...:8781instead ofhttp://...:8780.
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.
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 8780Now, 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/helloFor 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: */*
Contributions to HaRP are welcome. Feel free to open issues, discussions or submit pull requests with improvements, bug fixes, or new features.

