Adapting ExApps to HaRP

Summary

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 an FRP-based transport for ExApps and recommends copying start.sh into your ExApp image and using it as the container entrypoint. The script installs or starts the FRP client and executes your app process.

Key integration considerations

Steps needed to adapt an 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 launch the 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
ARG FRP_VERSION=0.71.1
ARG FRP_AMD64_SHA256=1d9875210972a5d1a62b2dc4c48df45ab0af05d3ca1b5e494a014428259df803
ARG FRP_ARM64_SHA256=8edf590b0c396fa42f9c147121a76af83cfc2002b08f5aef0ea1008c43482019

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_DIR="frp_${FRP_VERSION}_linux_${FRP_ARCH}"; \
    FRP_URL="https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/${FRP_DIR}.tar.gz"; \
    curl -fsSL "${FRP_URL}" -o /tmp/frp.tar.gz; \
    echo "${FRP_SHA256}  /tmp/frp.tar.gz" | sha256sum -c -; \
    tar -C /tmp -xzf /tmp/frp.tar.gz; \
    cp "/tmp/${FRP_DIR}/frpc" /usr/local/bin/frpc; \
    chmod +x /usr/local/bin/frpc; \
    rm -rf "/tmp/${FRP_DIR}" /tmp/frp.tar.gz

Running your ExApp with a non-root user

To run the main process as a non-root user while remaining compatible with HaRP and AppAPI, ensure the following:

  1. Keep image default user as root, drop to less privileged service user at runtime.

    • Make it easy for AppAPI to perform privileged operations (copying files, setting permissions, running update-ca-certificates) by leaving the container default user as root in the image.

    • Drop privileges for the main process in the ENTRYPOINT using gosu or su-exec so the runtime process runs as a non-root service user.

    Example snippet (Dockerfile):

    FROM python:3.12-alpine AS app
    
    ARG USER=serviceuser
    
    ENV USER=$USER
    ENV HOME=/home/$USER
    ENV GOSU_VERSION=1.19
    
    # ... other Dockerfile instructions ..
    
    # Install GOSU
    RUN set -eux; \
      \
      apk add --no-cache --virtual .gosu-deps \
        ca-certificates \
        dpkg \
        gnupg \
      ; \
      \
      dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
      wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
      wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
      \
      export GNUPGHOME="$(mktemp -d)"; \
      gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
      gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
      gpgconf --kill all; \
      rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
      \
      apk del --no-network .gosu-deps; \
      \
      chmod +x /usr/local/bin/gosu
    
    # Use gosu in combination with start.sh
    ENTRYPOINT ["/bin/sh", "-c", "exec gosu \"$USER\" /start.sh python3 -u main.py"]
  1. Ensure FRP config and cert paths are prepared for the service user

    • Create `/frpc.toml` or the directory that will contain it at image build time and set ownership to the service user so at runtime start.sh can write to it without requiring root.

    • Create directory `/certs/frp` and make it readable by the service user. AppAPI will copy cert files into that folder by using a docker cp ... command with the default container user (which is still root). By setting the directory owner to the service user, we will ensure the service user can read the certs at runtime.

Use some similar commands in your Dockerfile:

RUN touch /frpc.toml && \
  mkdir -p /certs/frp && \
  chown $USER:$USER /frpc.toml && \
  chown -R $USER:$USER /certs/frp && \
  chmod 600 /frpc.toml

Putting it all together:

FROM python:3.12-alpine AS app

ARG USER=serviceuser

ENV USER=$USER
ENV HOME=/home/$USER
ENV GOSU_VERSION=1.19

# Install dependencies and create service user. You might want to
# add additional packages depending on your app requirements.
# Make sure curl and FRP are installed.
RUN apk update && \
    apk add --no-cache curl frp ca-certificates && \
    adduser -D $USER && \
    touch /frpc.toml && \
    mkdir -p /certs/frp && \
    chown $USER:$USER /frpc.toml && \
    chown -R $USER:$USER /certs/frp && \
    chmod 600 /frpc.toml

# Install GOSU
RUN set -eux; \
  \
  apk add --no-cache --virtual .gosu-deps \
    ca-certificates \
    dpkg \
    gnupg \
  ; \
  \
  dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
  wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
  wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
  \
  export GNUPGHOME="$(mktemp -d)"; \
  gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
  gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
  gpgconf --kill all; \
  rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
  \
  apk del --no-network .gosu-deps; \
  \
  chmod +x /usr/local/bin/gosu

WORKDIR /app

# Copy your app code
COPY --chown=$USER:$USER <files> .

# Copy the start.sh script and make it executable
COPY --chown=$USER:$USER start.sh /start.sh
RUN chmod +x /start.sh && \
  chown -R $USER:$USER /app && \
  pip install -r requirements.txt

# Run the start.sh as entrypoint with non-root user and point it to your app
ENTRYPOINT ["/bin/sh", "-c", "exec gosu \"$USER\" /start.sh python3 -u main.py"]

Integration test example

An example test suite used to validate HaRP support for an ExApp is available in the workflow_ocr_backend repository (example commit that added HaRP support and tests):

This test demonstrates automated verification of FRP connection and runtime behaviour; it can be used as a reference when adding CI checks for HaRP compatibility.