I recently went through a surprisingly long debugging journey with Nextcloud Office on a Rocky Linux 10 server. The goal sounded simple:

Open and edit .ods, .xlsx, .docx, and other office files directly in the browser from Nextcloud, similar to the experience you get with Microsoft 365 or OnlyOffice.

The actual result at the beginning was much less satisfying: when double-clicking an .ods file in Nextcloud, the editor started loading, showed a spinner, and never came back. The browser was stuck, Nextcloud felt blocked, and I had to cancel the operation to get the instance usable again.

This post documents the full journey: what was broken, which assumptions turned out to be wrong, which errors were misleading, how we diagnosed them, why we eventually abandoned the built-in CODE setup, how we moved to a Docker-based Collabora setup, and how we plan to update it safely.


Initial Situation

The server setup was roughly this:

OS:              Rocky Linux 10
Web server:      Apache httpd
PHP:             PHP-FPM 8.3
Nextcloud path:  /var/www/html/nextcloud
Nextcloud URL:   https://www.jr-it-services.de/nextcloud
Office app:      Nextcloud Office / richdocuments
Built-in CODE:   richdocumentscode
SELinux:         Enforcing

The symptom was simple:

Opening an .ods file in Nextcloud starts the Office editor,
but the editor never finishes loading.

At first glance it looked like a browser or JavaScript problem. It was not.


Step 1: Checking the Nextcloud Office Configuration

The first thing we checked was the Nextcloud Office configuration via occ.

cd /var/www/html/nextcloud

sudo -u apache php occ app:list | egrep -i "richdocuments|collabora|onlyoffice|documentserver|office"

sudo -u apache php occ config:app:get richdocuments wopi_url
sudo -u apache php occ config:app:get richdocuments public_wopi_url

sudo -u apache php occ config:system:get overwrite.cli.url
sudo -u apache php occ config:system:get trusted_domains

The relevant output showed that we had the Nextcloud Office apps installed:

richdocuments
richdocumentscode

So the setup was using Nextcloud Office with built-in CODE, not a separate Collabora server.

The configured WOPI URL looked like this:

https://www.jr-it-services.de/nextcloud/apps/richdocumentscode/proxy.php?req=

That looked reasonable for built-in CODE.

But another value was wrong:

overwrite.cli.url = http://202.61.199.155/nextcloud

The public browser URL was:

https://www.jr-it-services.de/nextcloud

This mismatch is important. Nextcloud, Collabora/CODE, and the browser must agree on the same public URL scheme and host. HTTP plus raw IP address was not what the browser used.

So we fixed the core Nextcloud overwrite settings:

cd /var/www/html/nextcloud

sudo cp config/config.php config/config.php.bak.$(date +%F-%H%M%S)

sudo -u apache php occ config:system:set overwrite.cli.url --value="https://www.jr-it-services.de/nextcloud"
sudo -u apache php occ config:system:set overwriteprotocol --value="https"
sudo -u apache php occ config:system:set overwritehost --value="www.jr-it-services.de"
sudo -u apache php occ config:system:set overwritewebroot --value="/nextcloud"

sudo systemctl restart php-fpm httpd

Then we confirmed:

cd /var/www/html/nextcloud

sudo -u apache php occ config:system:get overwrite.cli.url
sudo -u apache php occ config:system:get overwriteprotocol
sudo -u apache php occ config:system:get overwritehost
sudo -u apache php occ config:system:get overwritewebroot

Expected result:

https://www.jr-it-services.de/nextcloud
https
www.jr-it-services.de
/nextcloud

This was a real issue and absolutely worth fixing, but it did not completely solve the Office loading problem.


Step 2: Running richdocuments:activate-config

Nextcloud provides a very useful command for checking the Office / Collabora integration:

cd /var/www/html/nextcloud

sudo -u apache php occ richdocuments:activate-config

Initially, this failed with:

Failed to fetch discovery endpoint from https://www.jr-it-services.de/nextcloud/apps/richdocumentscode/proxy.php?req=

Client error: `GET https://www.jr-it-services.de/nextcloud/apps/richdocumentscode/proxy.php?req=/hosting/discovery`
resulted in a `400 Bad Request` response:

<html><body>
<h1>Socket proxy error</h1>
<p>Error: no_glibc</p>
</body></html>

This looked like the server was missing glibc.

But that turned out to be misleading.


Step 3: Investigating the no_glibc Error

The built-in CODE app ships Collabora as an AppImage. So we checked the AppImage directly.

cd /var/www/html/nextcloud

APP="/var/www/html/nextcloud/apps/richdocumentscode/collabora/Collabora_Online.AppImage"

echo "=== System ==="
uname -a
ldd --version | head -1
php -r 'echo PHP_OS_FAMILY . PHP_EOL; echo php_uname("m") . PHP_EOL;'

echo "=== AppImage ==="
ls -lh "$APP"
file "$APP"

echo "=== Test as apache ==="
sudo -u apache bash -lc "LD_TRACE_LOADED_OBJECTS=1 '$APP'; echo EXIT:\$?"

The result was surprising:

ldd (GNU libc) 2.39
Linux
x86_64

/var/www/html/nextcloud/apps/richdocumentscode/collabora/Collabora_Online.AppImage:
ELF 64-bit LSB executable, x86-64

linux-vdso.so.1
libdl.so.2 => /lib64/libdl.so.2
libpthread.so.0 => /lib64/libpthread.so.0
libz.so.1 => /lib64/libz.so.1
libc.so.6 => /lib64/libc.so.6
/lib64/ld-linux-x86-64.so.2

EXIT:0

So glibc was present and the AppImage passed the basic loader test when run as the apache user from the shell.

The no_glibc message was not the real root cause.

It was a generic error emitted by the built-in CODE proxy when the AppImage startup failed.


Step 4: Testing from the Real PHP-FPM Web Context

Running something as apache from the shell is not always the same as running it through Apache/PHP-FPM.

So we created a temporary diagnostic PHP file inside the Nextcloud directory.

Important: this file was temporary and removed immediately after the test.

cd /var/www/html/nextcloud

TOKEN=$(openssl rand -hex 16)
DIAG="diag-code-$TOKEN.php"

sudo tee "$DIAG" > /dev/null <<'PHP'
<?php
if (($_GET['t'] ?? '') !== '__TOKEN__') {
    http_response_code(403);
    exit("forbidden\n");
}

header('Content-Type: text/plain');

echo "SAPI=" . php_sapi_name() . PHP_EOL;
echo "PHP_VERSION=" . PHP_VERSION . PHP_EOL;
echo "disable_functions=" . ini_get('disable_functions') . PHP_EOL;

foreach (['exec','shell_exec','proc_open','popen','passthru','system'] as $fn) {
    echo $fn . "=" . (function_exists($fn) ? "yes" : "no") . PHP_EOL;
}

$cmd = 'cd /var/www/html/nextcloud/apps/richdocumentscode/collabora && LD_TRACE_LOADED_OBJECTS=1 ./Collabora_Online.AppImage 2>&1';
echo "CMD=$cmd\n";

exec($cmd, $out, $code);

echo "CODE=$code\n";
echo implode("\n", $out) . "\n";
PHP

sudo sed -i "s/__TOKEN__/$TOKEN/g" "$DIAG"
sudo chown apache:apache "$DIAG"

curl -vk --max-time 30 "https://www.jr-it-services.de/nextcloud/$DIAG?t=$TOKEN"

sudo rm -f "$DIAG"

This revealed the real underlying error:

SAPI=fpm-fcgi
PHP_VERSION=8.3.31
disable_functions=
exec=yes
shell_exec=yes
proc_open=yes
popen=yes
passthru=yes
system=yes

CODE=127
fuse: failed to exec fusermount: No such file or directory
open dir error: No such file or directory

So the first real issue was not glibc.

It was missing FUSE tooling.


Step 5: Installing FUSE

We checked for fusermount:

command -v fusermount || true
command -v fusermount3 || true

ls -l /usr/bin/fusermount* /bin/fusermount* 2>/dev/null || true

rpm -q fuse fuse-libs fuse3 fuse3-libs || true

After installing the required packages:

sudo dnf install -y fuse fuse-libs fuse3 fuse3-libs

we confirmed:

/usr/bin/fusermount
/usr/bin/fusermount3

fuse-2.9.9-25.el10.x86_64
fuse-libs-2.9.9-25.el10.x86_64
fuse3-3.16.2-5.el10.x86_64
fuse3-libs-3.16.2-5.el10.x86_64

That fixed the missing binary, but it uncovered the next problem.


Step 6: PHP-FPM PATH Was Too Small

The AppImage tried to call fusermount, but PHP-FPM did not necessarily expose the same PATH as a shell session.

We checked the web context again and saw:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin

That was almost okay, but to make the runtime explicit, we added a stable PATH to PHP-FPM.

sudo cp /etc/php-fpm.d/www.conf /etc/php-fpm.d/www.conf.bak.$(date +%F-%H%M%S)

sudo bash -lc 'cat >> /etc/php-fpm.d/www.conf <<EOF

; Needed for Nextcloud richdocumentscode / AppImage / fusermount
env[PATH] = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
EOF'

sudo systemctl restart php-fpm httpd

The next diagnostic run changed the error:

CODE=127
fusermount: failed to open mountpoint for reading: Permission denied
open dir error: No such file or directory

So fusermount was now found, but FUSE mounting still failed.


Step 7: SELinux and FUSE

SELinux was enforcing:

getenforce

Output:

Enforcing

We checked the relevant SELinux booleans:

getsebool -a | grep -E "httpd.*tmp|httpd.*exec|httpd.*fuse"

Output:

httpd_execmem --> on
httpd_ssi_exec --> off
httpd_tmp_exec --> off
httpd_use_fusefs --> on

httpd_use_fusefs was already enabled, but httpd_tmp_exec was off.

Because AppImages often mount or extract runtime components into temporary directories, this mattered. We enabled it:

sudo setsebool -P httpd_tmp_exec on
sudo systemctl restart php-fpm httpd

Confirmed:

getsebool httpd_tmp_exec httpd_use_fusefs

Output:

httpd_tmp_exec --> on
httpd_use_fusefs --> on

This improved the situation, but the built-in CODE setup was still fragile.


Step 8: Trying AppImage Extract Mode

To avoid FUSE entirely, we tried AppImage extract mode:

sudo cp /etc/php-fpm.d/www.conf /etc/php-fpm.d/www.conf.bak.$(date +%F-%H%M%S)

sudo bash -lc 'cat >> /etc/php-fpm.d/www.conf <<EOF

; Fallback for AppImage without FUSE mount
env[APPIMAGE_EXTRACT_AND_RUN] = 1
env[TMPDIR] = /var/tmp
EOF'

sudo systemctl restart php-fpm httpd

Then we tested again:

APPIMAGE_EXTRACT_AND_RUN=1
TMPDIR=/var/tmp
CODE=127
Failed to run /tmp/appimage_extracted_813595cc0c835df8a0d104b7d20af607/AppRun: Permission denied

This was another rabbit hole.

We checked whether /tmp itself was mounted with noexec:

findmnt -no TARGET,FSTYPE,OPTIONS /tmp
findmnt -no TARGET,FSTYPE,OPTIONS /var/tmp

printf '#!/bin/sh\necho TMP_EXEC_OK\n' | sudo tee /tmp/nc-exec-test.sh >/dev/null
sudo chmod 755 /tmp/nc-exec-test.sh
/tmp/nc-exec-test.sh; echo "EXIT:$?"
sudo rm -f /tmp/nc-exec-test.sh

The result:

TMP_EXEC_OK
EXIT:0

So /tmp itself was executable from a shell.

Again, the problem was the web server context, SELinux, AppImage behavior, or a combination of these.

At this point we had already spent too much time dealing with:

built-in CODE
AppImage
FUSE
SELinux
PHP-FPM environment
temporary execution paths
misleading no_glibc errors

Even when we eventually got the built-in discovery endpoint to return XML, the browser-based editor still did not behave reliably.

That was the point where we decided to stop fighting the built-in CODE AppImage and move to a proper standalone Collabora container.


Why We Switched to Docker-Based Collabora

The main reason was not that Docker is magically faster.

The reason was operational simplicity.

Built-in CODE on this Rocky Linux 10 host involved too many moving parts:

Nextcloud Office app
richdocumentscode app
AppImage runtime
FUSE / fusermount
SELinux booleans
PHP-FPM environment variables
temporary execution paths
Apache/PHP restrictions

A standalone Collabora container gives us a much cleaner architecture:

Browser
  |
  | HTTPS
  v
Apache vHost: office.jr-it-services.de
  |
  | reverse proxy to localhost
  v
Collabora Docker container on 127.0.0.1:9980
  |
  | WOPI callback
  v
Nextcloud: https://www.jr-it-services.de/nextcloud

The container becomes its own isolated runtime. We no longer depend on PHP-FPM being able to execute an AppImage from the Nextcloud app directory.


Step 9: Starting the Collabora Docker Container

We started Collabora with Docker and bound it only to localhost:

sudo docker run -d \
  --name collabora-code \
  -p 127.0.0.1:9980:9980 \
  -e 'aliasgroup1=https://www.jr-it-services.de:443' \
  -e 'dictionaries=de_DE en_GB en_US' \
  --restart always \
  --cap-add MKNOD \
  collabora/code

Important details:

-p 127.0.0.1:9980:9980

This means Collabora is not exposed directly to the internet. Only Apache can reach it locally.

The aliasgroup1 value points to the Nextcloud host that Collabora is allowed to work with:

https://www.jr-it-services.de:443

Then we tested the container locally:

curl -vk https://127.0.0.1:9980/hosting/discovery \
  -o /tmp/local-office-discovery.xml

head -20 /tmp/local-office-discovery.xml

This returned:

HTTP/1.1 200 OK
Content-Type: text/xml

<wopi-discovery>
    <net-zone name="external-https">

So the container itself was healthy.


Step 10: Creating the Apache vHost

We wanted Collabora to be available at:

https://office.jr-it-services.de

At first, opening that URL in the browser showed the WordPress site. That meant Apache did not yet route the subdomain to a dedicated vHost.

We checked the vHost configuration:

sudo httpd -t -D DUMP_VHOSTS

Eventually we got the desired entry:

port 443 namevhost office.jr-it-services.de (/etc/httpd/conf.d/office.jr-it-services.de.conf:6)

The Apache vHost looked like this:

<VirtualHost *:80>
    ServerName office.jr-it-services.de
    Redirect permanent / https://office.jr-it-services.de/
</VirtualHost>

<VirtualHost *:443>
    ServerName office.jr-it-services.de

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/office.jr-it-services.de/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/office.jr-it-services.de/privkey.pem

    AllowEncodedSlashes NoDecode
    SSLProxyEngine On
    ProxyPreserveHost On

    SSLProxyVerify None
    SSLProxyCheckPeerCN Off
    SSLProxyCheckPeerName Off
    SSLProxyCheckPeerExpire Off

    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set X-Forwarded-Ssl "on"

    ProxyPass        /browser https://127.0.0.1:9980/browser retry=0
    ProxyPassReverse /browser https://127.0.0.1:9980/browser

    ProxyPass        /hosting/discovery https://127.0.0.1:9980/hosting/discovery retry=0
    ProxyPassReverse /hosting/discovery https://127.0.0.1:9980/hosting/discovery

    ProxyPass        /hosting/capabilities https://127.0.0.1:9980/hosting/capabilities retry=0
    ProxyPassReverse /hosting/capabilities https://127.0.0.1:9980/hosting/capabilities

    ProxyPassMatch   "/cool/(.*)/ws$" wss://127.0.0.1:9980/cool/$1/ws nocanon

    ProxyPass        /cool/adminws wss://127.0.0.1:9980/cool/adminws

    ProxyPass        /cool https://127.0.0.1:9980/cool
    ProxyPassReverse /cool https://127.0.0.1:9980/cool

    ProxyPass        /lool https://127.0.0.1:9980/cool
    ProxyPassReverse /lool https://127.0.0.1:9980/cool
</VirtualHost>

We also enabled Apache network proxying under SELinux:

sudo setsebool -P httpd_can_network_connect on

Then:

sudo apachectl configtest
sudo systemctl reload httpd

Output:

Syntax OK

Step 11: TLS and Certbot

When trying to run Certbot, we discovered that a certificate for office.jr-it-services.de already existed:

sudo certbot --apache -d office.jr-it-services.de

Certbot said:

Certificate not yet due for renewal

You have an existing certificate that has exactly the same domains or certificate name you requested
and isn't close to expiry.

1: Attempt to reinstall this existing certificate
2: Renew & replace the certificate

The correct choice was:

1

But Certbot then said:

We were unable to find a vhost with a ServerName or Address of office.jr-it-services.de.

This was because the vHost did not exist yet at that moment. So the correct thing was not to attach the certificate to the wrong existing vHost, but to create the Apache vHost manually and reference the already existing certificate:

SSLCertificateFile /etc/letsencrypt/live/office.jr-it-services.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/office.jr-it-services.de/privkey.pem

After that, TLS worked correctly.


Step 12: Testing the Reverse Proxy

First, we tested the vHost locally with SNI:

curl -vk --resolve office.jr-it-services.de:443:127.0.0.1 \
  https://office.jr-it-services.de/hosting/discovery \
  -o /tmp/office-discovery-local-vhost.xml

head -20 /tmp/office-discovery-local-vhost.xml

This returned:

HTTP/1.1 200 OK
Content-Type: text/xml

<wopi-discovery>
    <net-zone name="external-https">

Most importantly, the URLs inside the discovery XML were now correct:

<app favIconUrl="https://office.jr-it-services.de/browser/de013a57f9/images/x-office-document.svg" name="writer">
    <action default="true" ext="odt" name="edit" urlsrc="https://office.jr-it-services.de/browser/de013a57f9/cool.html?"/>
</app>

Then we tested externally without --resolve:

curl -v https://office.jr-it-services.de/hosting/discovery \
  -o /tmp/office-discovery.xml

head -20 /tmp/office-discovery.xml

TLS validation succeeded:

subjectAltName: "office.jr-it-services.de" matches cert's "office.jr-it-services.de"
OpenSSL verify result: 0
SSL certificate verified via OpenSSL.
HTTP/1.1 200 OK
Content-Type: text/xml
<wopi-discovery>

We also checked capabilities:

curl -v https://office.jr-it-services.de/hosting/capabilities

Output:

{
  "convert-to": {
    "available": false
  },
  "hasDocumentSigningSupport": true,
  "hasMobileSupport": true,
  "hasProxyPrefix": false,
  "hasSettingIframeSupport": true,
  "hasTemplateSaveAs": false,
  "hasTemplateSource": true,
  "hasWASMSupport": false,
  "hasWopiAccessCheck": true,
  "hasZoteroSupport": true,
  "productKitVersion": "26.04.2.1",
  "productKitVersionHash": "de013a57",
  "productName": "Collabora Online Development Edition",
  "productVersion": "26.04.2.1",
  "productVersionHash": "de013a57f9",
  "serverId": "D1DF1625"
}

At that point we knew:

DNS works
TLS works
Apache vHost works
Reverse proxy works
Collabora Docker works
Discovery works
Capabilities works

Step 13: Pointing Nextcloud to Docker Collabora

Now we changed Nextcloud Office to use the external Collabora endpoint:

cd /var/www/html/nextcloud

sudo -u apache php occ config:app:set richdocuments wopi_url --value="https://office.jr-it-services.de"
sudo -u apache php occ config:app:set richdocuments public_wopi_url --value="https://office.jr-it-services.de"

sudo -u apache php occ config:app:get richdocuments wopi_url
sudo -u apache php occ config:app:get richdocuments public_wopi_url

Output:

https://office.jr-it-services.de
https://office.jr-it-services.de

Then:

sudo -u apache php occ richdocuments:activate-config<br>

Success:

 Reset callback url autodetect
Checking configuration
🛈 Configured WOPI URL: https://office.jr-it-services.de
🛈 Configured public WOPI URL: https://office.jr-it-services.de
🛈 Configured callback URL:

 Fetched /hosting/discovery endpoint
 Valid mimetype response
 Valid capabilities entry
 Fetched /hosting/capabilities endpoint
 Detected WOPI server: Collabora Online Development Edition 26.04.2.1

Collabora URL:
  https://office.jr-it-services.de

Collabora public URL:
  https://office.jr-it-services.de

Callback URL:
  autodetected

This was the clean target state.


Step 14: Disabling Built-in CODE

After switching to Docker Collabora, the built-in CODE app was no longer needed.

We tried:

sudo -u apache php occ app:disable richdocumentscode

The result was:

No such app enabled: richdocumentscode

That was fine. It meant the built-in CODE app was already disabled or no longer active.

The important app is:

richdocuments

That one must remain enabled.

To verify:

cd /var/www/html/nextcloud

sudo -u apache php occ app:list | egrep -i "richdocuments|code|onlyoffice"

Docker Permission Issue

At one point, running Docker as the normal user failed:

docker ps

Output:

permission denied while trying to connect to the docker API at unix:///var/run/docker.sock

That is expected if the user is not in the docker group.

The safe approach is simply:

sudo docker ps

Alternatively, one can add the user to the Docker group:

sudo usermod -aG docker johannes

Then log out and back in, or run:

newgrp docker

However, membership in the Docker group is effectively root-like access. On a server, using sudo docker ... explicitly is often the safer operational habit.


Moving from docker run to Docker Compose

The initial docker run command worked, but for updates and maintenance, Docker Compose is much cleaner.

Current file:

cat /opt/collabora/docker-compose.yml

Initial Compose content:

services:
  collabora-code:
    image: collabora/code:latest
    container_name: collabora-code
    restart: always
    ports:
      - "127.0.0.1:9980:9980"
    environment:
      aliasgroup1: "https://www.jr-it-services.de:443"
      dictionaries: "de_DE en_GB en_US"
    cap_add:
      - MKNOD

This is already a good minimal configuration.

We slightly extended it to this:

services:
  collabora-code:
    image: collabora/code:latest
    container_name: collabora-code
    restart: always

    ports:
      - "127.0.0.1:9980:9980"

    environment:
      aliasgroup1: "https://www.jr-it-services.de:443"
      dictionaries: "de_DE en_GB en_US"

    cap_add:
      - MKNOD

    ulimits:
      nofile:
        soft: 65536
        hard: 65536

The nofile limit is not a magic performance booster, but it gives Collabora more room for open files and socket connections.

Apply it:

cd /opt/collabora

sudo cp docker-compose.yml docker-compose.yml.bak.$(date +%F-%H%M%S)

sudo docker compose config
sudo docker compose up -d

Then verify:

curl -v https://office.jr-it-services.de/hosting/capabilities

cd /var/www/html/nextcloud
sudo -u apache php occ richdocuments:activate-config

Optional: Enabling the Collabora Admin Console

For monitoring, the Collabora admin console can be enabled by setting an admin user and password.

Better than placing the password directly in the Compose file is using an .env file.

Create:

sudo nano /opt/collabora/.env

Example:

COLLABORA_ADMIN_USER=admin
COLLABORA_ADMIN_PASSWORD=replace-this-with-a-long-random-password

Secure it:

sudo chmod 600 /opt/collabora/.env
sudo chown root:root /opt/collabora/.env

Then update Compose:

services:
  collabora-code:
    image: collabora/code:latest
    container_name: collabora-code
    restart: always

    ports:
      - "127.0.0.1:9980:9980"

    env_file:
      - .env

    environment:
      aliasgroup1: "https://www.jr-it-services.de:443"
      dictionaries: "de_DE en_GB en_US"
      username: "${COLLABORA_ADMIN_USER}"
      password: "${COLLABORA_ADMIN_PASSWORD}"

    cap_add:
      - MKNOD

    ulimits:
      nofile:
        soft: 65536
        hard: 65536

Apply:

cd /opt/collabora

sudo docker compose config
sudo docker compose up -d

The admin console is then available at:

https://office.jr-it-services.de/browser/dist/admin/admin.html

The Apache vHost already includes the required /cool/adminws WebSocket proxy:

ProxyPass /cool/adminws wss://127.0.0.1:9980/cool/adminws

Updating Collabora Safely

The simplest manual update is:

cd /opt/collabora

sudo docker compose pull
sudo docker compose up -d
sudo docker image prune -f

Then verify:

curl -v https://office.jr-it-services.de/hosting/capabilities

cd /var/www/html/nextcloud
sudo -u apache php occ richdocuments:activate-config

A more robust approach is a scripted update.

Create:

sudo nano /usr/local/sbin/update-collabora-code

Content:

#!/usr/bin/env bash
set -euo pipefail

COMPOSE_DIR="/opt/collabora"
LOG_FILE="/var/log/collabora-code-update.log"

{
  echo "============================================================"
  echo "Collabora CODE update started: $(date -Is)"

  cd "$COMPOSE_DIR"

  echo
  echo "Current container:"
  docker ps --filter "name=collabora-code" --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"

  echo
  echo "Pulling latest image..."
  docker compose pull

  echo
  echo "Recreating container if needed..."
  docker compose up -d

  echo
  echo "Waiting for Collabora..."
  sleep 10

  echo
  echo "Testing capabilities endpoint..."
  curl -fsS https://office.jr-it-services.de/hosting/capabilities >/tmp/collabora-capabilities.json

  echo
  echo "Testing discovery endpoint..."
  curl -fsS https://office.jr-it-services.de/hosting/discovery >/tmp/collabora-discovery.xml

  echo
  echo "Current capabilities:"
  cat /tmp/collabora-capabilities.json
  echo

  echo
  echo "Nextcloud richdocuments check..."
  cd /var/www/html/nextcloud
  sudo -u apache php occ richdocuments:activate-config

  echo
  echo "Pruning old unused Docker images..."
  docker image prune -f

  echo
  echo "Collabora CODE update finished successfully: $(date -Is)"
} >> "$LOG_FILE" 2>&1

Make it executable:

sudo chmod 750 /usr/local/sbin/update-collabora-code

Test it manually:

sudo /usr/local/sbin/update-collabora-code
sudo tail -120 /var/log/collabora-code-update.log

Optional Weekly systemd Timer

Create the service:

sudo nano /etc/systemd/system/update-collabora-code.service

Content:

[Unit]
Description=Update Collabora CODE Docker container

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/update-collabora-code

Create the timer:

sudo nano /etc/systemd/system/update-collabora-code.timer

Content:

[Unit]
Description=Weekly Collabora CODE Docker update

[Timer]
OnCalendar=Sun 04:15
Persistent=true

[Install]
WantedBy=timers.target

Enable it:

sudo systemctl daemon-reload
sudo systemctl enable --now update-collabora-code.timer

Check:

systemctl list-timers | grep collabora

This gives a controlled update process with logging and validation instead of blindly replacing the container.


Why Docker and Not Podman?

Podman would have worked.

On Rocky Linux, Podman is actually a very natural choice because it integrates nicely with systemd, SELinux, and rootless/container-native workflows.

A Podman run command could look like this:

sudo podman run -d \
  --name collabora-code \
  -p 127.0.0.1:9980:9980 \
  -e aliasgroup1=https://www.jr-it-services.de:443 \
  -e dictionaries="de_DE en_GB en_US" \
  --restart=always \
  --cap-add MKNOD \
  docker.io/collabora/code:latest

A more systemd-native Podman Quadlet setup could look like this:

[Unit]
Description=Collabora CODE Container

[Container]
Image=docker.io/collabora/code:latest
ContainerName=collabora-code
PublishPort=127.0.0.1:9980:9980
Environment=aliasgroup1=https://www.jr-it-services.de:443
Environment=dictionaries=de_DE en_GB en_US
AddCapability=MKNOD
AutoUpdate=registry

[Service]
Restart=always
TimeoutStartSec=900

[Install]
WantedBy=multi-user.target

That would be stored as:

/etc/containers/systemd/collabora-code.container

Then:

sudo systemctl daemon-reload
sudo systemctl enable --now collabora-code.service

So why did we stay with Docker?

Because after a long debugging session, the goal was to reach and preserve a stable state.

At the time Docker was already working:

Collabora container running
Apache reverse proxy working
TLS working
Nextcloud detecting Collabora successfully

Migrating immediately to Podman would not have improved performance in any meaningful way. Both Docker and Podman use the same underlying Linux kernel features: namespaces, cgroups, networking, and overlay storage.

For this workload, the bottlenecks are not Docker vs. Podman. The relevant factors are:

CPU
RAM
document size
number of concurrent users
LibreOffice processes inside Collabora
WebSocket stability
Apache reverse proxy behavior

So the decision was:

Do not change the container runtime immediately after fixing the actual problem.

Docker remains perfectly fine here.

Podman may still be a good future migration target, especially if the server is standardized around Rocky/RHEL-native container tooling. But there is no urgent need to migrate just for performance.


Basic Runtime Monitoring

To see how much memory Collabora uses:

sudo docker stats collabora-code

One-time snapshot:

sudo docker stats --no-stream collabora-code

Formatted output:

sudo docker stats --no-stream \
  --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}" \
  collabora-code

Example output:

NAME              MEM USAGE / LIMIT     MEM %     CPU %
collabora-code    850MiB / 62.5GiB      1.33%     2.10%

Show processes inside the container:

sudo docker top collabora-code

Or inspect from inside:

sudo docker exec -it collabora-code bash

ps aux --sort=-%mem | head -20

Host memory:

free -h

Live view:

watch -n 2 'sudo docker stats --no-stream collabora-code'

Performance Notes

The biggest performance improvement was not a tuning flag.

It was replacing built-in CODE with standalone Collabora.

Built-in CODE was blocked by:

AppImage startup
FUSE
SELinux
PHP-FPM execution context
temporary extraction issues

The Docker-based setup avoids that entire class of problems.

The current Compose setup should not artificially limit CPU or RAM:

# Do not add unnecessary limits like this unless you have a reason:
# deploy:
#   resources:
#     limits:
#       cpus: "2"
#       memory: 2G

For a private or small-office server with enough CPU and RAM, let Collabora use the resources it needs.

A sensible small optimization is keeping dictionaries limited:

dictionaries: "de_DE en_GB en_US"

Do not load dozens of dictionaries unless needed.

Another useful operational tweak is the nofile ulimit:

ulimits:
  nofile:
    soft: 65536
    hard: 65536

But in practice, performance should be measured before tuning.

Use:

sudo docker stats collabora-code

while opening and editing real documents.


Mistakes and Lessons Learned

Mistake 1: Trusting the no_glibc Error Too Much

The error said:

Error: no_glibc

But glibc was present:

ldd (GNU libc) 2.39

The AppImage loader test returned:

EXIT:0

The real problems were later shown to be FUSE, PATH, SELinux, and execution context.

Lesson:

Do not stop at the first high-level error message.
Reproduce the failing command in the same runtime context.

Mistake 2: Testing as apache in Shell and Assuming PHP-FPM Is the Same

This worked:

sudo -u apache bash -lc "LD_TRACE_LOADED_OBJECTS=1 '$APP'; echo EXIT:\$?"

But PHP-FPM still failed.

Lesson:

The shell environment of a user is not the same as the PHP-FPM web runtime.

The temporary diagnostic PHP script was what revealed the real problem.

Mistake 3: Spending Too Long on Built-in CODE

Built-in CODE can be convenient, but on this server it pulled us into:

AppImage internals
FUSE
SELinux booleans
PHP-FPM env vars
/tmp execution

Lesson:

At some point, replacing a fragile architecture is better than fixing every fragile edge case.

Mistake 4: Assuming the Browser Root URL Matters

When opening:

https://office.jr-it-services.de/

the browser showed WordPress.

That looked bad, but the real endpoint was:

https://office.jr-it-services.de/hosting/discovery

Once /hosting/discovery and /hosting/capabilities worked, the Collabora service was functional.

Lesson:

For Collabora, test the WOPI endpoints, not just the root URL.

Mistake 5: Letting Certbot Choose a Wrong vHost

Certbot found the certificate but no matching vHost:

We were unable to find a vhost with a ServerName or Address of office.jr-it-services.de.

The correct action was not to attach the certificate to some unrelated vHost. The correct action was to cancel, create the proper vHost manually, and reference the existing certificate.

Lesson:

When Certbot cannot find the right vHost, fix Apache first.

Final Architecture

The final architecture looks like this:

Browser
  |
  | HTTPS
  v
https://www.jr-it-services.de/nextcloud
  |
  | Nextcloud Office / WOPI
  v
https://office.jr-it-services.de
  |
  | Apache Reverse Proxy
  v
https://127.0.0.1:9980
  |
  v
Docker container: collabora/code

Nextcloud config:

richdocuments wopi_url:
https://office.jr-it-services.de

richdocuments public_wopi_url:
https://office.jr-it-services.de

Collabora Docker:

services:
  collabora-code:
    image: collabora/code:latest
    container_name: collabora-code
    restart: always
    ports:
      - "127.0.0.1:9980:9980"
    environment:
      aliasgroup1: "https://www.jr-it-services.de:443"
      dictionaries: "de_DE en_GB en_US"
    cap_add:
      - MKNOD

Validation:

curl -v https://office.jr-it-services.de/hosting/discovery \
  -o /tmp/office-discovery.xml

curl -v https://office.jr-it-services.de/hosting/capabilities

cd /var/www/html/nextcloud
sudo -u apache php occ richdocuments:activate-config

Expected:

✓ Fetched /hosting/discovery endpoint
✓ Valid mimetype response
✓ Valid capabilities entry
✓ Fetched /hosting/capabilities endpoint
✓ Detected WOPI server: Collabora Online Development Edition 26.04.2.1

Summary

This started as a simple “Nextcloud Office keeps spinning” problem and turned into a full-stack debugging session across Nextcloud, Apache, PHP-FPM, SELinux, AppImage, FUSE, TLS, Docker, and WOPI.

The first fix was correcting the Nextcloud public URL configuration:

from:
http://202.61.199.155/nextcloud

to:
https://www.jr-it-services.de/nextcloud

Then the built-in CODE error appeared:

Socket proxy error
Error: no_glibc

That error was misleading. The real problems were discovered only by testing the AppImage from the PHP-FPM web context:

fuse: failed to exec fusermount
fusermount: Permission denied
AppRun: Permission denied

Although several of these issues could be partially fixed with packages, PHP-FPM environment variables, and SELinux booleans, the built-in CODE approach remained fragile.

The better solution was moving to a standalone Collabora Docker container behind its own Apache vHost:

https://office.jr-it-services.de

Once Docker Collabora was running, the important endpoints worked cleanly:

/hosting/discovery
/hosting/capabilities

Nextcloud accepted the new WOPI server and detected:

Collabora Online Development Edition 26.04.2.1

Docker was chosen over Podman not because it is faster, but because it was already working, well-documented for this use case, and avoided introducing another migration step immediately after solving the main issue. Podman remains a reasonable future option, especially on Rocky Linux, but there is no performance-driven need to migrate.

The final lesson is simple:

Built-in CODE is convenient until the AppImage runtime becomes the problem.
For a real Linux server with Apache, SELinux, and PHP-FPM, a standalone Collabora container is cleaner, easier to debug, easier to update, and much easier to operate.

Views: 0

From Endless Spinner to Working Collabora: Debugging Nextcloud Office on Rocky Linux 10

Johannes Rest


.NET Architekt und Entwickler


Beitragsnavigation


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert