|
| 1 | +Adapting ExApps to HaRP |
| 2 | +====================== |
| 3 | + |
| 4 | +.. mermaid:: |
| 5 | + |
| 6 | + graph LR |
| 7 | + Client[Client] -->|connects| NC[Nextcloud Proxy] |
| 8 | + NC -->|connects| HaRP[HaRP - FRP proxy] |
| 9 | + HaRP -->|forwards| ExApp[ExApp container] |
| 10 | + ExApp -->|runs FRP client| HaRP |
| 11 | + AppAPI[Nextcloud AppAPI] -->|manage certs| ExApp |
| 12 | + |
| 13 | +Summary |
| 14 | +------- |
| 15 | + |
| 16 | +HaRP is a reverse proxy system designed to simplify the deployment workflow |
| 17 | +for Nextcloud 32’s AppAPI. |
| 18 | + |
| 19 | +It enables direct communication between clients and ExApps, bypassing |
| 20 | +the Nextcloud instance to improve performance and reduce the complexity |
| 21 | +traditionally associated with `DockerSocketProxy` setups. |
| 22 | + |
| 23 | +HaRP provides an `FRP-based <https://github.com/fatedier/frp>`_ |
| 24 | +transport for ExApps and recommends copying |
| 25 | +`start.sh <https://github.com/nextcloud/HaRP/blob/main/exapps_dev/start.sh>`_ |
| 26 | +into your ExApp image and using it as the container entrypoint. |
| 27 | +The script installs or starts the FRP client and executes your app process. |
| 28 | + |
| 29 | +.. warning:: |
| 30 | + |
| 31 | + We strongly recommend starting support for HaRP in ExApps from the start |
| 32 | + of Nextcloud 32, as the old `DSP <https://github.com/nextcloud/docker-socket-proxy>`_ |
| 33 | + way will be deprecated and marked for removal in Nextcloud 35. |
| 34 | + |
| 35 | + Adding HaRP support is fully compatible with the existing DSP system, |
| 36 | + so you won’t need to maintain two separate release types of your ExApp. |
| 37 | + |
| 38 | +Key integration considerations |
| 39 | +------------------------------ |
| 40 | + |
| 41 | +- **Connecting to HaRP with FRPC**: Your ExApp does not need to expose any ports to the host |
| 42 | + or be reachable from the Nextcloud server. The FRP client (`FRPC`) inside your ExApp |
| 43 | + container will create an outbound connection to HaRP, which will proxy |
| 44 | + requests from clients to your ExApp. |
| 45 | +- **File permissions**: AppAPI may copy certificate files into the container and |
| 46 | + execute commands inside it. If your container runs the main process as a |
| 47 | + non-root service user, AppAPI's file writes or execs may fail unless the |
| 48 | + receiving paths are writable/readable by that user. |
| 49 | +- **Certs and FRP config**: HaRP expects FRP cert files to be accessible under |
| 50 | + `/certs/frp` (client.crt, client.key, ca.crt). The FRP client configuration |
| 51 | + path used by many example `start.sh` scripts is `/frpc.toml`. |
| 52 | +- **Root-only commands**: Some setup steps (for example updating CA bundles with |
| 53 | + `update-ca-certificates`) require root; AppAPI may need to run those using |
| 54 | + `docker exec ...` when setting up containers. |
| 55 | + |
| 56 | +Steps needed to adapt an ExApp |
| 57 | +-------------------------------------- |
| 58 | + |
| 59 | +1. Copy the `start.sh <https://github.com/nextcloud/HaRP/blob/main/exapps_dev/start.sh>`_ |
| 60 | +script from the exapps_dev folder of the HaRP repository into your Docker image |
| 61 | +(e.g., using a `COPY` instruction). |
| 62 | + |
| 63 | +2. In your ExApp's Dockerfile, set the `ENTRYPOINT` to execute `start.sh` followed by |
| 64 | +the **command and arguments required to launch** your actual application. The `start.sh` |
| 65 | +script will launch the FRP client if needed and then use `exec` to |
| 66 | +run the command you provide as arguments. |
| 67 | + |
| 68 | +3. Ensure the `curl` command-line utility is installed in your ExApp's Docker image, |
| 69 | +as it's needed by the following script to download the FRP client. |
| 70 | + |
| 71 | +4. Add the following lines to your Dockerfile to automatically include the FRP |
| 72 | +client binaries in your Docker image: |
| 73 | + |
| 74 | + .. code-block:: dockerfile |
| 75 | + |
| 76 | + # Download and install FRP client |
| 77 | + RUN set -ex; \ |
| 78 | + ARCH=$(uname -m); \ |
| 79 | + if [ "$ARCH" = "aarch64" ]; then \ |
| 80 | + FRP_URL="https://raw.githubusercontent.com/nextcloud/HaRP/main/exapps_dev/frp_0.61.1_linux_arm64.tar.gz"; \ |
| 81 | + else \ |
| 82 | + FRP_URL="https://raw.githubusercontent.com/nextcloud/HaRP/main/exapps_dev/frp_0.61.1_linux_amd64.tar.gz"; \ |
| 83 | + fi; \ |
| 84 | + echo "Downloading FRP client from $FRP_URL"; \ |
| 85 | + curl -L "$FRP_URL" -o /tmp/frp.tar.gz; \ |
| 86 | + tar -C /tmp -xzf /tmp/frp.tar.gz; \ |
| 87 | + mv /tmp/frp_0.61.1_linux_* /tmp/frp; \ |
| 88 | + cp /tmp/frp/frpc /usr/local/bin/frpc; \ |
| 89 | + chmod +x /usr/local/bin/frpc; \ |
| 90 | + rm -rf /tmp/frp /tmp/frp.tar.gz |
| 91 | +
|
| 92 | + .. note:: |
| 93 | + |
| 94 | + For Alpine 3.21 Linux you can just install FRP from repo using apk add frp command. |
| 95 | + |
| 96 | +Running your ExApp with a non-root user |
| 97 | +-------------------------------------- |
| 98 | + |
| 99 | +.. note:: |
| 100 | + |
| 101 | + In `Docker Build best practices <https://docs.docker.com/build/building/best-practices/#user>`_, |
| 102 | + it is recommended to run application containers as non-root users for security reasons |
| 103 | + whenever possible. |
| 104 | + |
| 105 | +To run the main process as a non-root user while remaining compatible with HaRP and AppAPI, |
| 106 | +ensure the following: |
| 107 | + |
| 108 | +1. Keep image default user as `root`, drop to less privileged service user at runtime. |
| 109 | + |
| 110 | + - Make it easy for AppAPI to perform privileged operations (copying files, |
| 111 | + setting permissions, running `update-ca-certificates`) by leaving the |
| 112 | + container default user as `root` in the image. |
| 113 | + - Drop privileges for the main process in the `ENTRYPOINT` using `gosu` |
| 114 | + or `su-exec` so the runtime process runs as a non-root service user. |
| 115 | + |
| 116 | + Example snippet (Dockerfile): |
| 117 | + |
| 118 | + .. code-block:: dockerfile |
| 119 | +
|
| 120 | + FROM python:3.12-alpine AS app |
| 121 | +
|
| 122 | + ARG USER=serviceuser |
| 123 | +
|
| 124 | + ENV USER=$USER |
| 125 | + ENV HOME=/home/$USER |
| 126 | + ENV GOSU_VERSION=1.19 |
| 127 | +
|
| 128 | + # ... other Dockerfile instructions .. |
| 129 | +
|
| 130 | + # Install GOSU |
| 131 | + RUN set -eux; \ |
| 132 | + \ |
| 133 | + apk add --no-cache --virtual .gosu-deps \ |
| 134 | + ca-certificates \ |
| 135 | + dpkg \ |
| 136 | + gnupg \ |
| 137 | + ; \ |
| 138 | + \ |
| 139 | + dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \ |
| 140 | + wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \ |
| 141 | + wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \ |
| 142 | + \ |
| 143 | + export GNUPGHOME="$(mktemp -d)"; \ |
| 144 | + gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \ |
| 145 | + gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \ |
| 146 | + gpgconf --kill all; \ |
| 147 | + rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \ |
| 148 | + \ |
| 149 | + apk del --no-network .gosu-deps; \ |
| 150 | + \ |
| 151 | + chmod +x /usr/local/bin/gosu |
| 152 | +
|
| 153 | + # Use gosu in combination with start.sh |
| 154 | + ENTRYPOINT ["/bin/sh", "-c", "exec gosu \"$USER\" /start.sh python3 -u main.py"] |
| 155 | +
|
| 156 | +.. note:: |
| 157 | + |
| 158 | + See the `gosu documentation <https://github.com/tianon/gosu/blob/master/INSTALL.md>`_ |
| 159 | + for more details. |
| 160 | + |
| 161 | +2. Ensure FRP config and cert paths are prepared for the service user |
| 162 | + |
| 163 | + - **Create `/frpc.toml`** or the directory that will contain it at image build |
| 164 | + time and set ownership to the service user so at runtime `start.sh` can |
| 165 | + write to it without requiring root. |
| 166 | + - **Create directory `/certs/frp`** and make it readable by the service user. |
| 167 | + AppAPI will copy cert files into that folder by using a `docker cp ...` command |
| 168 | + with the default container user (which is still `root`). By setting the directory |
| 169 | + owner to the service user, we will ensure the service user can read the certs at runtime. |
| 170 | + |
| 171 | + Use some similar commands in your Dockerfile: |
| 172 | + |
| 173 | + .. code-block:: dockerfile |
| 174 | + |
| 175 | + RUN touch /frpc.toml && \ |
| 176 | + mkdir -p /certs/frp && \ |
| 177 | + chown $USER:$USER /frpc.toml && \ |
| 178 | + chown -R $USER:$USER /certs/frp && \ |
| 179 | + chmod 600 /frpc.toml |
| 180 | +
|
| 181 | +**Putting it all together:** |
| 182 | + |
| 183 | +.. code-block:: dockerfile |
| 184 | +
|
| 185 | + FROM python:3.12-alpine AS app |
| 186 | +
|
| 187 | + ARG USER=serviceuser |
| 188 | +
|
| 189 | + ENV USER=$USER |
| 190 | + ENV HOME=/home/$USER |
| 191 | + ENV GOSU_VERSION=1.19 |
| 192 | +
|
| 193 | + # Install dependencies and create service user. You might want to |
| 194 | + # add additional packages depending on your app requirements. |
| 195 | + # Make sure curl and FRP are installed. |
| 196 | + RUN apk update && \ |
| 197 | + apk add --no-cache curl frp ca-certificates && \ |
| 198 | + adduser -D $USER && \ |
| 199 | + touch /frpc.toml && \ |
| 200 | + mkdir -p /certs/frp && \ |
| 201 | + chown $USER:$USER /frpc.toml && \ |
| 202 | + chown -R $USER:$USER /certs/frp && \ |
| 203 | + chmod 600 /frpc.toml |
| 204 | +
|
| 205 | + # Install GOSU |
| 206 | + RUN set -eux; \ |
| 207 | + \ |
| 208 | + apk add --no-cache --virtual .gosu-deps \ |
| 209 | + ca-certificates \ |
| 210 | + dpkg \ |
| 211 | + gnupg \ |
| 212 | + ; \ |
| 213 | + \ |
| 214 | + dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \ |
| 215 | + wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \ |
| 216 | + wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \ |
| 217 | + \ |
| 218 | + export GNUPGHOME="$(mktemp -d)"; \ |
| 219 | + gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \ |
| 220 | + gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \ |
| 221 | + gpgconf --kill all; \ |
| 222 | + rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \ |
| 223 | + \ |
| 224 | + apk del --no-network .gosu-deps; \ |
| 225 | + \ |
| 226 | + chmod +x /usr/local/bin/gosu |
| 227 | +
|
| 228 | + WORKDIR /app |
| 229 | +
|
| 230 | + # Copy your app code |
| 231 | + COPY --chown=$USER:$USER <files> . |
| 232 | +
|
| 233 | + # Copy the start.sh script and make it executable |
| 234 | + COPY --chown=$USER:$USER start.sh /start.sh |
| 235 | + RUN chmod +x /start.sh && \ |
| 236 | + chown -R $USER:$USER /app && \ |
| 237 | + pip install -r requirements.txt |
| 238 | +
|
| 239 | + # Run the start.sh as entrypoint with non-root user and point it to your app |
| 240 | + ENTRYPOINT ["/bin/sh", "-c", "exec gosu \"$USER\" /start.sh python3 -u main.py"] |
| 241 | +
|
| 242 | +Integration test example |
| 243 | +--------------------- |
| 244 | + |
| 245 | +An example test suite used to validate HaRP support for an ExApp is available |
| 246 | +in the `workflow_ocr_backend` repository (example commit that added HaRP |
| 247 | +support and tests): |
| 248 | + |
| 249 | +- https://github.com/R0Wi-DEV/workflow_ocr_backend/blob/f5ae6efb6e4a3307328a188898968abf000511ab/test/test_harp_integration.py |
| 250 | + |
| 251 | +This test demonstrates automated verification of FRP connection and runtime |
| 252 | +behaviour; it can be used as a reference when adding CI checks for HaRP |
| 253 | +compatibility. |
0 commit comments