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
