From 987191ac14a4192b16e721d47e8b731ce278477b Mon Sep 17 00:00:00 2001
From: Oleksandr Piskun
Date: Wed, 18 Feb 2026 14:45:44 +0200
Subject: [PATCH] feat(app-api): add HaRP container (#7493)
Signed-off-by: Oleksander Piskun
Signed-off-by: bigcat88
Signed-off-by: Oleksandr Piskun
Signed-off-by: Simon L.
Co-authored-by: Simon L.
---
Containers/apache/Caddyfile | 5 ++
Containers/nextcloud/entrypoint.sh | 4 +-
manual-install/readme.md | 2 +-
manual-install/update-yaml.sh | 4 ++
php/containers.json | 55 +++++++++++++++++--
php/public/containers-form-submit.js | 11 +++-
php/public/disable-harp.js | 7 +++
php/public/index.php | 1 +
php/src/ContainerDefinitionFetcher.php | 8 +++
.../Controller/ConfigurationController.php | 1 +
php/src/Data/ConfigurationManager.php | 6 ++
.../includes/optional-containers.twig | 17 +++++-
12 files changed, 112 insertions(+), 9 deletions(-)
create mode 100644 php/public/disable-harp.js
diff --git a/Containers/apache/Caddyfile b/Containers/apache/Caddyfile
index 4b92d807..92e84b49 100644
--- a/Containers/apache/Caddyfile
+++ b/Containers/apache/Caddyfile
@@ -58,6 +58,11 @@ http://{$APACHE_HOST}:23973, # For Collabora callback and WOPI requests, see con
reverse_proxy {$WHITEBOARD_HOST}:3002
}
+ # HaRP (ExApps)
+ route /exapps/* {
+ reverse_proxy {$HARP_HOST}:8780
+ }
+
# Nextcloud
route {
header Strict-Transport-Security max-age=31536000;
diff --git a/Containers/nextcloud/entrypoint.sh b/Containers/nextcloud/entrypoint.sh
index d4b4f253..760c5b58 100644
--- a/Containers/nextcloud/entrypoint.sh
+++ b/Containers/nextcloud/entrypoint.sh
@@ -1025,13 +1025,13 @@ else
fi
fi
-# Docker socket proxy
+# Docker socket proxy / HaRP
# app_api is a shipped app
if [ -d "/var/www/html/custom_apps/app_api" ]; then
php /var/www/html/occ app:disable app_api
rm -r "/var/www/html/custom_apps/app_api"
fi
-if [ "$DOCKER_SOCKET_PROXY_ENABLED" = 'yes' ]; then
+if [ "$DOCKER_SOCKET_PROXY_ENABLED" = 'yes' ] || [ "$HARP_ENABLED" = 'yes' ]; then
if [ "$(php /var/www/html/occ config:app:get app_api enabled)" != "yes" ]; then
php /var/www/html/occ app:enable app_api
fi
diff --git a/manual-install/readme.md b/manual-install/readme.md
index ea2c2978..6908db09 100644
--- a/manual-install/readme.md
+++ b/manual-install/readme.md
@@ -12,7 +12,7 @@ You can run the containers that are build for AIO with docker-compose. This come
- You lose the AIO interface
- You lose update notifications and automatic updates
- You lose all AIO backup and restore features
-- You lose the built-in [Docker Socket Proxy container](https://github.com/nextcloud/docker-socket-proxy#readme) (needed for [Nextcloud App API](https://github.com/nextcloud/app_api#nextcloud-appapi))
+- You lose the built-in [Docker Socket Proxy container](https://github.com/nextcloud/docker-socket-proxy#readme) and [HaRP container](https://github.com/nextcloud/HaRP) (needed for [Nextcloud App API](https://github.com/nextcloud/app_api#nextcloud-appapi))
- You lose all community containers: https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers
- **You need to know what you are doing, especially when modifying the compose.yaml file**
- For updating, you need to strictly follow the at the bottom described update routine
diff --git a/manual-install/update-yaml.sh b/manual-install/update-yaml.sh
index 928275da..0f0c630f 100644
--- a/manual-install/update-yaml.sh
+++ b/manual-install/update-yaml.sh
@@ -27,6 +27,8 @@ OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "next
OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-borgbackup"))')"
OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-docker-socket-proxy"))')"
OUTPUT="$(echo "$OUTPUT" | jq '.services[] |= if has("depends_on") then .depends_on |= if contains(["nextcloud-aio-docker-socket-proxy"]) then del(.[index("nextcloud-aio-docker-socket-proxy")]) else . end else . end')"
+OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-harp"))')"
+OUTPUT="$(echo "$OUTPUT" | jq '.services[] |= if has("depends_on") then .depends_on |= if contains(["nextcloud-aio-harp"]) then del(.[index("nextcloud-aio-harp")]) else . end else . end')"
OUTPUT="$(echo "$OUTPUT" | jq '.services[] |= if has("depends_on") then .depends_on |= map({ (.): { "condition": "service_started", "required": false } }) else . end' | jq '.services[] |= if has("depends_on") then .depends_on |= reduce .[] as $item ({}; . + $item) else . end')"
sudo snap install yq
@@ -45,6 +47,8 @@ sed -i 's|- ip_binding: |- |' containers.yml
sed -i '/AIO_TOKEN/d' containers.yml
sed -i '/AIO_URL/d' containers.yml
sed -i '/DOCKER_SOCKET_PROXY_ENABLED/d' containers.yml
+sed -i '/HARP_ENABLED/d' containers.yml
+sed -i '/HP_SHARED_KEY/d' containers.yml
sed -i '/ADDITIONAL_TRUSTED_PROXY/d' containers.yml
sed -i '/TURN_DOMAIN/d' containers.yml
sed -i '/NC_AIO_VERSION/d' containers.yml
diff --git a/php/containers.json b/php/containers.json
index 8e9218ac..56a64240 100644
--- a/php/containers.json
+++ b/php/containers.json
@@ -10,6 +10,7 @@
"nextcloud-aio-talk",
"nextcloud-aio-notify-push",
"nextcloud-aio-whiteboard",
+ "nextcloud-aio-harp",
"nextcloud-aio-nextcloud"
],
"display_name": "Apache",
@@ -49,7 +50,8 @@
"APACHE_MAX_SIZE=%APACHE_MAX_SIZE%",
"APACHE_MAX_TIME=%NEXTCLOUD_MAX_TIME%",
"NOTIFY_PUSH_HOST=nextcloud-aio-notify-push",
- "WHITEBOARD_HOST=nextcloud-aio-whiteboard"
+ "WHITEBOARD_HOST=nextcloud-aio-whiteboard",
+ "HARP_HOST=nextcloud-aio-harp"
],
"volumes": [
{
@@ -172,7 +174,8 @@
"SIGNALING_SECRET",
"FULLTEXTSEARCH_PASSWORD",
"IMAGINARY_SECRET",
- "WHITEBOARD_SECRET"
+ "WHITEBOARD_SECRET",
+ "HP_SHARED_KEY"
],
"volumes": [
{
@@ -258,7 +261,9 @@
"THIS_IS_AIO=true",
"IMAGINARY_SECRET=%IMAGINARY_SECRET%",
"WHITEBOARD_SECRET=%WHITEBOARD_SECRET%",
- "WHITEBOARD_ENABLED=%WHITEBOARD_ENABLED%"
+ "WHITEBOARD_ENABLED=%WHITEBOARD_ENABLED%",
+ "HARP_ENABLED=%HARP_ENABLED%",
+ "HP_SHARED_KEY=%HP_SHARED_KEY%"
],
"stop_grace_period": 600,
"restart": "unless-stopped",
@@ -824,7 +829,7 @@
{
"container_name": "nextcloud-aio-docker-socket-proxy",
"image_tag": "%AIO_CHANNEL%",
- "display_name": "Docker Socket Proxy",
+ "display_name": "Docker Socket Proxy (deprecated)",
"image": "ghcr.io/nextcloud-releases/aio-docker-socket-proxy",
"init": true,
"internal_port": "2375",
@@ -847,6 +852,48 @@
"NET_RAW"
]
},
+ {
+ "container_name": "nextcloud-aio-harp",
+ "image_tag": "release",
+ "display_name": "HaRP",
+ "image": "ghcr.io/nextcloud/nextcloud-appapi-harp",
+ "init": true,
+ "internal_port": "8780",
+ "expose": [
+ "8780"
+ ],
+ "environment": [
+ "HP_SHARED_KEY=%HP_SHARED_KEY%",
+ "NC_INSTANCE_URL=https://%NC_DOMAIN%",
+ "HP_LOG_LEVEL=warning",
+ "HP_FRP_DISABLE_TLS=true",
+ "TZ=%TIMEZONE%"
+ ],
+ "secrets": [
+ "HP_SHARED_KEY"
+ ],
+ "volumes": [
+ {
+ "source": "%WATCHTOWER_DOCKER_SOCKET_PATH%",
+ "destination": "/var/run/docker.sock",
+ "writeable": false
+ },
+ {
+ "source": "nextcloud_aio_harp",
+ "destination": "/certs",
+ "writeable": true
+ }
+ ],
+ "restart": "unless-stopped",
+ "read_only": true,
+ "tmpfs": [
+ "/tmp",
+ "/run/harp"
+ ],
+ "cap_drop": [
+ "NET_RAW"
+ ]
+ },
{
"container_name": "nextcloud-aio-whiteboard",
"image_tag": "%AIO_CHANNEL%",
diff --git a/php/public/containers-form-submit.js b/php/public/containers-form-submit.js
index 2c8e0e63..cda62d60 100644
--- a/php/public/containers-form-submit.js
+++ b/php/public/containers-form-submit.js
@@ -121,13 +121,22 @@ document.addEventListener("DOMContentLoaded", function () {
function handleDockerSocketProxyWarning() {
if (document.getElementById("docker-socket-proxy").checked) {
- alert('⚠️ Warning! Enabling this container comes with possible Security problems since you are exposing the docker socket and all its privileges to the Nextcloud container. Enable this only if you are sure what you are doing!');
+ alert('⚠️ The docker socket proxy container is deprecated. Please use the HaRP (High-availability Reverse Proxy for Nextcloud ExApps) instead!');
+ document.getElementById("docker-socket-proxy").checked = false
+ }
+ }
+
+ function handleHarpWarning() {
+ if (document.getElementById("harp").checked) {
+ alert('⚠️ Warning! Enabling this container comes with possible Security problems since you are exposing the docker socket and all its privileges to the HaRP container. Enable this only if you are sure what you are doing!');
+ document.getElementById("docker-socket-proxy").checked = false
}
}
// Initialize event listeners for specific behaviors
document.getElementById("talk").addEventListener('change', handleTalkVisibility);
document.getElementById("docker-socket-proxy").addEventListener('change', handleDockerSocketProxyWarning);
+ document.getElementById("harp").addEventListener('change', handleHarpWarning);
// Initialize talk-recording visibility on page load
handleTalkVisibility(); // Ensure talk-recording is correctly initialized
diff --git a/php/public/disable-harp.js b/php/public/disable-harp.js
new file mode 100644
index 00000000..fb3b992b
--- /dev/null
+++ b/php/public/disable-harp.js
@@ -0,0 +1,7 @@
+document.addEventListener("DOMContentLoaded", function(event) {
+ // HaRP
+ let harp = document.getElementById("harp");
+ if (harp) {
+ harp.disabled = true;
+ }
+});
diff --git a/php/public/index.php b/php/public/index.php
index cc06bb90..e0ac8dec 100644
--- a/php/public/index.php
+++ b/php/public/index.php
@@ -136,6 +136,7 @@ $app->get('/containers', function (Request $request, Response $response, array $
'is_nvidia_gpu_enabled' => $configurationManager->enableNvidiaGpu,
'is_talk_recording_enabled' => $configurationManager->isTalkRecordingEnabled,
'is_docker_socket_proxy_enabled' => $configurationManager->isDockerSocketProxyEnabled,
+ 'is_harp_enabled' => $configurationManager->isHarpEnabled,
'is_whiteboard_enabled' => $configurationManager->isWhiteboardEnabled,
'community_containers' => $configurationManager->listAvailableCommunityContainers(),
'community_containers_enabled' => $configurationManager->aioCommunityContainers,
diff --git a/php/src/ContainerDefinitionFetcher.php b/php/src/ContainerDefinitionFetcher.php
index bacc322b..5481cd00 100644
--- a/php/src/ContainerDefinitionFetcher.php
+++ b/php/src/ContainerDefinitionFetcher.php
@@ -91,6 +91,10 @@ readonly class ContainerDefinitionFetcher {
if (!$this->configurationManager->isDockerSocketProxyEnabled) {
continue;
}
+ } elseif ($entry['container_name'] === 'nextcloud-aio-harp') {
+ if (!$this->configurationManager->isHarpEnabled) {
+ continue;
+ }
} elseif ($entry['container_name'] === 'nextcloud-aio-whiteboard') {
if (!$this->configurationManager->isWhiteboardEnabled) {
continue;
@@ -200,6 +204,10 @@ readonly class ContainerDefinitionFetcher {
if (!$this->configurationManager->isDockerSocketProxyEnabled) {
continue;
}
+ } elseif ($value === 'nextcloud-aio-harp') {
+ if (!$this->configurationManager->isHarpEnabled) {
+ continue;
+ }
} elseif ($value === 'nextcloud-aio-whiteboard') {
if (!$this->configurationManager->isWhiteboardEnabled) {
continue;
diff --git a/php/src/Controller/ConfigurationController.php b/php/src/Controller/ConfigurationController.php
index 2cc56b65..4dbe3ae2 100644
--- a/php/src/Controller/ConfigurationController.php
+++ b/php/src/Controller/ConfigurationController.php
@@ -96,6 +96,7 @@ readonly class ConfigurationController {
$this->configurationManager->isImaginaryEnabled = isset($request->getParsedBody()['imaginary']);
$this->configurationManager->isFulltextsearchEnabled = isset($request->getParsedBody()['fulltextsearch']);
$this->configurationManager->isDockerSocketProxyEnabled = isset($request->getParsedBody()['docker-socket-proxy']);
+ $this->configurationManager->isHarpEnabled = isset($request->getParsedBody()['harp']);
$this->configurationManager->isWhiteboardEnabled = isset($request->getParsedBody()['whiteboard']);
}
diff --git a/php/src/Data/ConfigurationManager.php b/php/src/Data/ConfigurationManager.php
index 9c03cedb..4b23f746 100644
--- a/php/src/Data/ConfigurationManager.php
+++ b/php/src/Data/ConfigurationManager.php
@@ -30,6 +30,11 @@ class ConfigurationManager
set { $this->set('isDockerSocketProxyEnabled', $value); }
}
+ public bool $isHarpEnabled {
+ get => $this->get('isHarpEnabled', false);
+ set { $this->set('isHarpEnabled', $value); }
+ }
+
public bool $isWhiteboardEnabled {
// Type-cast because old configs could have 1/0 for this key.
get => (bool) $this->get('isWhiteboardEnabled', true);
@@ -1035,6 +1040,7 @@ class ConfigurationManager
'IMAGINARY_ENABLED' => $this->isImaginaryEnabled ? 'yes' : '',
'FULLTEXTSEARCH_ENABLED' => $this->isFulltextsearchEnabled ? 'yes' : '',
'DOCKER_SOCKET_PROXY_ENABLED' => $this->isDockerSocketProxyEnabled ? 'yes' : '',
+ 'HARP_ENABLED' => $this->isHarpEnabled ? 'yes' : '',
'NEXTCLOUD_UPLOAD_LIMIT' => $this->nextcloudUploadLimit,
'NEXTCLOUD_MEMORY_LIMIT' => $this->nextcloudMemoryLimit,
'NEXTCLOUD_MAX_TIME' => $this->nextcloudMaxTime,
diff --git a/php/templates/includes/optional-containers.twig b/php/templates/includes/optional-containers.twig
index 9651cf29..08f98634 100644
--- a/php/templates/includes/optional-containers.twig
+++ b/php/templates/includes/optional-containers.twig
@@ -196,7 +196,21 @@
data-initial-state="false"
{% endif %}
>
-
+
+
+
+
+
+