When a small team wants a Nextcloud replacement without a PHP stack or a database server, OpenCloud is the one we would hand them. It is a single Go binary forked from ownCloud Infinite Scale, and it keeps files and metadata on plain disk. The official opencloud-compose project wires it to Traefik, Let’s Encrypt and Collabora Online in one Docker Compose stack. You install OpenCloud with Docker Compose by cloning that project, setting a handful of values in .env, and running docker compose up -d; the rest of this guide is what happens around those three steps.
Beyond the install, it covers Collabora editing, the Excalidraw whiteboard, the desktop client, a measured RAM and disk footprint, a restore drill and the 7.x to 8.x upgrade. Every step was tested in October 2026 on Ubuntu 26.04 and Debian 13 with OpenCloud 8.1.0, with the Docker steps repeated on Ubuntu 24.04. Our lab has no public IP, so Traefik’s ACME client ran against Pebble, the test CA Let’s Encrypt publishes.
How the OpenCloud stack fits together
OpenCloud replaces the PHP, database and Redis tiers of a Nextcloud install with one Go process that serves the web UI, WebDAV, the identity provider and the storage service. Users and spaces live in an embedded LDAP store, files live under /var/lib/opencloud, and per-file metadata is written to extended attributes on those files. That last detail matters later, because a backup that drops extended attributes restores files OpenCloud can no longer read.
The opencloud-compose project puts Traefik in front of everything. Only Traefik publishes ports to the host, and it routes cloud.example.com to OpenCloud and collabora.example.com to Collabora Online on a private bridge network called opencloud-net:

Collabora talks back to OpenCloud over the public name, so both DNS names must resolve from inside the containers too. The compose files handle that by giving the Traefik container network aliases for both domains, which is why you don’t need to touch /etc/hosts on the server. Older guides still create a third wopiserver name; current opencloud-compose serves WOPI on the main domain under /wopi, so two records are enough.
Preparing the server, DNS and firewall
Plan for a fresh VM that runs nothing else on ports 80 and 443, because Traefik binds both. The server needs the following:
- Ubuntu 26.04, Ubuntu 24.04 or Debian 13, 64-bit
- 2 vCPU and 4 GB RAM for a small team with Collabora
- 20 GB for the OS and container images, plus your data
- A public IP, or a port forward for TCP 80 and 443
- Two DNS A records pointing at that IP
Our lab used 4 vCPU and 8 GB, which is a comfortable floor for following along rather than a production sizing. The measured section below shows the whole stack idling at about 0.8 GB of container memory. Collabora is the largest consumer, so the real sizing drivers are how many people edit office documents at the same moment and how much data you plan to store. OpenCloud’s own requirements page lists 512 MB for up to 10 users without Collabora and 8 GB for around 1,000 users.
Create these two records at your DNS provider before you start, because Traefik requests certificates on the first start and Let’s Encrypt has to reach the server by name:
cloud.example.com: the OpenCloud web UI, WebDAV and desktop synccollabora.example.com: Collabora Online
Check that both names resolve to your server’s public address from outside, using a public resolver rather than the local stub:
dig +short cloud.example.com @1.1.1.1
dig +short collabora.example.com @1.1.1.1
Both commands should print the same public IP. If either comes back empty, wait for the record to propagate before going further. Let’s Encrypt rate limits failed validations, and a rushed first start against an unresolved name is the usual way to hit them, so a minute of patience here saves an hour of waiting later.
On Ubuntu, open SSH, HTTP and HTTPS in UFW. The OpenSSH profile only covers port 22, so if your SSH daemon listens elsewhere, allow that port first or the enable step locks you out. Port 80 stays open after setup because Traefik answers renewal challenges on it and redirects everything else to HTTPS:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw status
The status output on our Ubuntu 26.04 server listed the three rules for IPv4 and IPv6:
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
80/tcp (v6) ALLOW Anywhere (v6)
443/tcp (v6) ALLOW Anywhere (v6)
Docker writes its own iptables rules for published ports, so UFW does not filter 80 and 443 once Traefik publishes them. Docker documents this under packet filtering and firewalls. That is fine here because those two ports must be public anyway. The stack publishes nothing else, which ss -tlnp confirmed on our server: on non-loopback addresses, apart from SSH, only docker-proxy listened, on 80 and 443. Debian 13 does not install UFW by default, so run sudo apt-get install -y ufw before the commands above, or rely on your provider’s firewall for everything except 22, 80 and 443. For the rest of the host, our UFW command reference covers rate limits and per-source rules.
Installing Docker Engine on Ubuntu or Debian
OpenCloud’s compose files need Docker Compose v2 or newer, so install Docker Engine from Docker’s own repository rather than the distro’s docker.io package. The commands below read ID and VERSION_CODENAME from /etc/os-release, so the same block works on Ubuntu 26.04, Ubuntu 24.04 and Debian 13 without edits:
sudo apt-get update
sudo apt-get install -y ca-certificates curl git
. /etc/os-release
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL "https://download.docker.com/linux/${ID}/gpg" -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
printf 'Types: deb\nURIs: https://download.docker.com/linux/%s\nSuites: %s\nComponents: stable\nSigned-By: /etc/apt/keyrings/docker.asc\n' "${ID}" "${VERSION_CODENAME}" | sudo tee /etc/apt/sources.list.d/docker.sources
Docker already publishes a resolute suite for Ubuntu 26.04, so the repository resolves on the newest LTS too. Install the engine and the Compose plugin, then add your user to the docker group:
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker "$USER"
Log out and back in so the group change applies, because the current shell keeps its old group list. Then confirm the versions and that the daemon is running:
docker --version
docker compose version
systemctl is-active docker
All three of our test servers printed the same versions:
Docker version 29.9.2, build a82e528
Docker Compose version v5.6.1
active
One thing changed with recent Docker Engine releases that affects disk planning. Images are now stored by containerd under /var/lib/containerd, not under /var/lib/docker, while named volumes still live in /var/lib/docker/volumes. We mounted a separate data disk at /var/lib/docker on the Debian VM and its 3 GB root disk still filled up during the image pull, because the 2.6 GB of images went to the root filesystem. If you split disks, size the root disk for the images too.
Cloning opencloud-compose and setting the .env
Every reader-specific value in this guide appears several times, so export them once at the top of your SSH session. Pick strong passwords for the OpenCloud admin and the Collabora admin console, but leave out the pipe, ampersand, backslash and dollar characters and the sequence of a space followed by a hash. The sed command below treats the first three as syntax, and Docker Compose reads a dollar sign in .env as a variable and a space plus hash as the start of a comment:
export OC_DOMAIN="cloud.example.com"
export COLLABORA_DOMAIN="collabora.example.com"
export ACME_MAIL="[email protected]"
export ADMIN_PASS='Use-A-Long-Passphrase-2026!'
export COLLABORA_ADMIN_PASS='Another-Long-Passphrase-2026'
These values only last for the current shell. If you reconnect, run the block again before any later command that uses them, or the sed and openssl steps will write and check empty names. Clone the project and note the commit you are on:
git clone https://github.com/opencloud-eu/opencloud-compose.git
cd opencloud-compose
git log -1 --format='%h %cs %s'
The project has no tags or releases, so the commit is the only version marker. Its default image is opencloudeu/opencloud-rolling, the rolling channel that ships a new minor release every few weeks. OpenCloud’s release lifecycle pitches rolling at enthusiasts and keeps a separate Production track for production use. That slower channel lives under opencloudeu/opencloud, which was still on the 7.x line in October 2026. Write it down, because it tells you later which compose files your server was built from. Ours was the October 12 main branch, which defaults to the rolling OpenCloud image shown in the output below:
c957c36 2026-10-12 chore(deps): update traefik docker tag to v3.7.14 (#398)
Copy the example file and write your values into it. The last three expressions undo defaults that only suit a test box: the Collabora admin console password, which otherwise stays admin, Collabora’s certificate check, which the example file turns off, and INSECURE, which it sets to true for self-signed setups:
cp .env.example .env
sed -i \
-e "s|^OC_DOMAIN=.*|OC_DOMAIN=${OC_DOMAIN}|" \
-e "s|^COLLABORA_DOMAIN=.*|COLLABORA_DOMAIN=${COLLABORA_DOMAIN}|" \
-e "s|^TRAEFIK_ACME_MAIL=.*|TRAEFIK_ACME_MAIL=${ACME_MAIL}|" \
-e "s|^INITIAL_ADMIN_PASSWORD=.*|INITIAL_ADMIN_PASSWORD=${ADMIN_PASS}|" \
-e "s|^COLLABORA_ADMIN_PASSWORD=.*|COLLABORA_ADMIN_PASSWORD=${COLLABORA_ADMIN_PASS}|" \
-e "s|^COLLABORA_SSL_VERIFICATION=false|COLLABORA_SSL_VERIFICATION=true|" \
-e "s|^INSECURE=true|INSECURE=false|" \
.env
The Collabora admin console answers at https://collabora.example.com/browser/dist/admin/admin.html on the public name, so the default admin/admin login would be open to anyone who finds it. With the new password in place, our server answered 401 to admin:admin and 200 to the new password. Set INSECURE=false explicitly rather than commenting the line out, which is what the comment in .env.example suggests. The two compose files read it with different defaults: docker-compose.yml falls back to false, but weboffice/collabora.yml falls back to true for the WOPI connection, so a missing line leaves Collabora’s link to OpenCloud unverified.
Next, tell Compose which files make up the stack. COMPOSE_FILE is a colon-separated list that replaces the -f flags, and none of the example lines are uncommented, so append the full Collabora stack:
echo "COMPOSE_FILE=docker-compose.yml:yjs/yjs.yml:weboffice/collabora.yml:traefik/opencloud.yml:traefik/collabora.yml" >> .env
grep -nE '^(OC_DOMAIN|COLLABORA_DOMAIN|TRAEFIK_ACME_MAIL|INSECURE|COLLABORA_SSL_VERIFICATION|COMPOSE_FILE)=' .env
The grep shows each value once, at the line number where the example file defines it. A value that appears twice means a line was appended instead of replaced, and the later one wins:
8:INSECURE=false
47:[email protected]
101:OC_DOMAIN=cloud.example.com
221:COLLABORA_DOMAIN=collabora.example.com
238:COLLABORA_SSL_VERIFICATION=true
416:COMPOSE_FILE=docker-compose.yml:yjs/yjs.yml:weboffice/collabora.yml:traefik/opencloud.yml:traefik/collabora.yml
The five files do these jobs, so you know what to drop if you don’t want a piece:
docker-compose.yml: OpenCloud itself and its two volumesyjs/yjs.yml: real-time editing for Markdown, notes and whiteboardsweboffice/collabora.yml: Collabora Online and its WOPI proof keytraefik/opencloud.yml: Traefik, Let’s Encrypt and the cloud routetraefik/collabora.yml: the collabora route
INITIAL_ADMIN_PASSWORD is read on the first start only. Change it later from the user menu in the web UI, not in .env; if the variable is empty on the first start, the OpenCloud container keeps restarting.
Issuing Let’s Encrypt certificates through Traefik
Traefik requests a certificate for each router on the first start, using the HTTP-01 challenge on port 80. Point it at the Let’s Encrypt staging CA first, because a typo in DNS then costs you nothing against the production rate limits:
sed -i "s|^TRAEFIK_ACME_CASERVER=.*|TRAEFIK_ACME_CASERVER=https://acme-staging-v02.api.letsencrypt.org/directory|" .env
docker compose up -d
The first pull downloads about 2.7 GB of images, most of it the 1.84 GB Collabora image, and took about a minute on our lab link. Once the containers are up, ask Traefik which certificate it serves for each name:
echo | openssl s_client -connect 127.0.0.1:443 -servername "${OC_DOMAIN}" 2>/dev/null | openssl x509 -noout -issuer -ext subjectAltName -dates
A staging certificate names a “(STAGING)” issuer. Our run printed the Pebble test CA instead, with the right name in the SAN and a 90-day lifetime, which proves the HTTP-01 exchange between Traefik and the CA completed:
issuer=CN=Pebble Intermediate CA 76e342
X509v3 Subject Alternative Name: critical
DNS:cloud.example.com
notBefore=Oct 26 20:07:45 2026 GMT
notAfter=Jan 24 20:07:44 2027 GMT
While the staging certificate is in place, you will see TLS errors in the OpenCloud log. With INSECURE=false it refuses the untrusted staging chain on its own public name, and the errors stop once production certificates arrive. Collabora refuses the staging chain for the same reason, so documents will not open until then either. If you still see “TRAEFIK DEFAULT CERT” after a minute, Traefik couldn’t validate. Check docker compose logs traefik, and confirm port 80 is reachable from outside and that both names resolve publicly. Once staging works, clear the CA override, remove the staging account and certificates, and start again so Traefik requests real ones:
docker compose down
sed -i "s|^TRAEFIK_ACME_CASERVER=.*|TRAEFIK_ACME_CASERVER=|" .env
sudo rm certs/acme.json
docker compose up -d
Traefik keeps everything ACME-related in certs/acme.json inside the project directory, owned by root with mode 600. Back that file up with the volumes later; losing it only means Traefik requests fresh certificates, but a rebuild that requests them repeatedly can run into the weekly duplicate-certificate limit.
In our lab, the only difference from the commands above was the CA URL: Traefik pointed at Pebble on the compose network and trusted Pebble’s root through LEGO_CA_CERTIFICATES. We also mounted that root into the OpenCloud and Collabora containers so INSECURE=false and Collabora’s certificate verification could be tested as a public server runs them. With real Let’s Encrypt certificates none of that is needed, because the containers already trust its roots.
Starting OpenCloud and logging in
With real certificates in place, check that all four services are up and that Collabora’s health check passes:
docker compose ps --format "table {{.Service}}\t{{.Image}}\t{{.Status}}"
OpenCloud has no health check of its own in the compose file, so only Collabora and Yjs report healthy. A restart counter climbing on the opencloud line is the thing to watch for, and it usually means an empty admin password:
SERVICE IMAGE STATUS
collabora collabora/code:26.04.5.1.1 Up 6 minutes (healthy)
opencloud opencloudeu/opencloud-rolling:8.1.0 Up 6 minutes
traefik traefik:v3.7.14 Up About an hour
yjs opencloudeu/yjs:1.1.0 Up About an hour (healthy)
The status.php endpoint gives the running version without logging in, which is handy in monitoring and after upgrades:
curl -s "https://${OC_DOMAIN}/status.php" | grep -E 'productversion|edition'
Our server reported the rolling 8.1.0 build:
"edition": "rolling",
"productversion": "8.1.0",
On the first start, OpenCloud logged one error, WopiDiscovery: wopi app url failed with unexpected code with HTTP 404, because it asked Collabora for its discovery file before Collabora had finished starting. The collaboration service retried on its own, and /app/list listed CollaboraOnline for every office type a few seconds later, so you can ignore that line on a first boot.
Open https://cloud.example.com in a browser. OpenCloud redirects to its built-in identity provider, where you log in as admin with the password you exported:

After login, you land in the Personal space. The footer in the sidebar shows the running version and whether an update is available, which reads from the same check that CHECK_FOR_UPDATES controls in .env. Here it is after we uploaded a few files, created a document and a whiteboard, and synced a gigabyte from the desktop client:

Personal is only one kind of space. For shared team folders, open Spaces in the sidebar and use New to create a project space; ours, called Operations, was created in one step and showed up right away. The desktop client later reported a quota of 953.7 MB on that new space, so look at the quota in the space’s options before a team starts filling it; we left ours at that default:

That’s the core setup done: a working OpenCloud with HTTPS, Collabora wired in and an admin account. The next three sections are optional add-ons, and nothing in the core depends on them.
Editing documents with Collabora
Collabora is already part of the stack, so there’s nothing to install. In the web UI, choose New, then Document, Spreadsheet or Presentation, give the file a name and keep the extension. The document opens in Collabora inside the OpenCloud window:

When you open a file, OpenCloud hands the browser a Collabora URL plus a WOPI access token, and Collabora then fetches and saves the file through https://cloud.example.com/wopi. That callback is why the aliasgroup1 setting in weboffice/collabora.yml holds your OpenCloud domain. It is also why the COLLABORA_SSL_VERIFICATION=true edit matters: with it, a self-signed or broken certificate on the cloud name stops documents from loading instead of being accepted silently. On our server, with that edit and trusted certificates, the Collabora log showed ssl.ssl_verification: true, the same document reopened, and no SSL warnings appeared.
If you want to run Collabora on a separate host instead, our standalone Collabora Online setup covers that layout; for one team, keeping it in this stack is simpler.
Adding the Excalidraw whiteboard
The 8.1 release added an Excalidraw whiteboard that syncs live through the Yjs service, but it’s a web extension and not part of the compose stack. On a fresh install, the New menu has no Whiteboard entry until you add it. Extensions load from config/opencloud/apps, which the compose file mounts into the container. Detect the latest Excalidraw extension release and unpack it there:
sudo apt-get install -y unzip
EXCALIDRAW_VER=$(curl -s "https://api.github.com/repos/opencloud-eu/web-extensions/releases?per_page=50" | grep -o '"tag_name": *"excalidraw-v[^"]*"' | head -1 | sed 's/.*excalidraw-v//; s/"//')
echo "${EXCALIDRAW_VER}"
curl -sL "https://github.com/opencloud-eu/web-extensions/releases/download/excalidraw-v${EXCALIDRAW_VER}/excalidraw-${EXCALIDRAW_VER}.zip" -o /tmp/excalidraw.zip
unzip -q -o /tmp/excalidraw.zip -d config/opencloud/apps
rm /tmp/excalidraw.zip
docker compose restart opencloud
The detection printed 1.1.0 on our run, and config/opencloud/apps then held an excalidraw directory next to the bundled maps extension. After the restart, reload the web UI and Whiteboard appears in the New menu:

Whiteboards are saved as .excalidraw files in the space where you create them. We drew a small rack plan, closed the tab, and reopened it from a separate browser session, and every shape and label came back:

Use the save icon next to the file name before closing the editor. When we pressed Escape after typing a label, the editor treated it as a request to close and asked whether to save the unsaved changes, so a habit of saving explicitly avoids losing a sketch.
Connecting the OpenCloud desktop client
The desktop client keeps a local folder per space in sync, the way the Nextcloud and ownCloud clients do, and it runs on each team member’s workstation, not on the server. On Linux it ships as an AppImage on GitHub; Windows and macOS builds are linked from the same release page. On an Ubuntu or Debian desktop, download the latest release and add the two libraries it needs:
DESKTOP_VER=$(curl -s https://api.github.com/repos/opencloud-eu/desktop/releases/latest | grep -m1 '"tag_name"' | cut -d '"' -f 4)
echo "${DESKTOP_VER}"
mkdir -p ~/Applications
curl -L "https://github.com/opencloud-eu/desktop/releases/download/${DESKTOP_VER}/OpenCloud_Desktop-${DESKTOP_VER}-linux-gcc-x86_64.AppImage" -o ~/Applications/OpenCloud_Desktop.AppImage
chmod +x ~/Applications/OpenCloud_Desktop.AppImage
sudo apt-get install -y libfuse2t64 libopengl0
Check that the AppImage starts and see which build you got. Running it with --version first catches missing libraries before you are halfway through the setup wizard:
~/Applications/OpenCloud_Desktop.AppImage --version
We ran the client on the Ubuntu 26.04 lab server inside an Xvfb display, and it printed its build, Qt and OpenSSL versions:
OpenCloud OpenCloud 4.0.0.3642
https://github.com/opencloud-eu/desktop/commit/9f701e6db2e101cb3247d3331647a60266e83b0f
Libraries Qt 6.11.1, OpenSSL 3.6.3 9 Jun 2026
Launch it, enter https://cloud.example.com as the server address, and the client moves to a browser login step. It opens your default browser on OpenCloud’s sign-in page and catches the result on a local port, so no password is typed into the client itself:

After you approve the login, the client creates ~/OpenCloud with one folder per space, Personal, Shares and every project space you’re a member of:

Two things showed up in our run that you won’t see on a normal desktop. The client asked whether to trust the certificate, because our lab CA isn’t a public one; with Let’s Encrypt that prompt doesn’t appear. And our browser login was driven by a scripted Chromium, since the lab VM has no desktop session, so we can’t speak for how each desktop environment handles the browser hand-off.
Measuring OpenCloud RAM and disk usage
The OpenCloud process is small, and Collabora is what you size the server for. We read docker stats at four points on the Ubuntu 26.04 server. The first came 4 minutes after the first start, the second at the peak of a 1 GB sync from the desktop client, and the third with one document open in Collabora. The last came about 10 minutes after the load test, a few minutes after a restart for a config change. The memory column is the container’s cgroup usage minus inactive file cache, which is close to what the processes actually hold:
| Container | Idle after start | Peak during 1 GB sync | One document open | Settled after load |
|---|---|---|---|---|
| OpenCloud | 280.5 MiB | 186.1 MiB | 180.2 MiB | 216.2 MiB |
| Collabora | 429.7 MiB | 439.2 MiB | 496.4 MiB | 484.4 MiB |
| Yjs | 40.0 MiB | 35.2 MiB | 35.1 MiB | 40.3 MiB |
| Traefik | 21.9 MiB | 32.6 MiB | 33.8 MiB | 30.4 MiB |
| Stack total | 772 MiB | 693 MiB (sum of peaks) | 746 MiB | 771 MiB |
The whole stack sits under 0.8 GB at rest, and Collabora accounts for more than half of it before anyone opens a file. Opening one document lifted Collabora from its 430 to 439 MiB idle range to 496 MiB in our run, so budget roughly 60 MiB per open document on top of the baseline, more for large spreadsheets. On the Debian 13 server, which ran OpenCloud and Traefik without Collabora, the two containers used 284 MiB together, which is the number to compare against a bare Nextcloud and database pair. OpenCloud’s own figure was highest in the first minutes after the very first start and settled lower once the stack had been running, so don’t size from a reading taken during startup.

The sync itself is easy on the server. The desktop client pushed 301 files totalling 1000 MB in 18.5 seconds, and OpenCloud peaked at 214% CPU on 4 vCPUs while it wrote the blobs. That test ran on the same VM as the server, so it measures the write path of OpenCloud, not a network link.
On disk, the numbers are just as plain:
- Container images: 2.7 GB, of which Collabora is 1.84 GB
opencloud-dataafter the 1 GB upload: 1,013 MBopencloud-config: 12 KB
Data takes the same space as the files you upload, with no compression or deduplication, so size the data disk from your team’s file total plus room for versions and trash. The config volume is tiny, but it holds the generated opencloud.yaml with every service secret, so treat it as more valuable than its size suggests.
Backing up the data and config volumes
OpenCloud has no database dump step: a backup is the two Docker volumes, taken while the stack is stopped so files and metadata agree. Compose names the volumes after the project directory, so with the default checkout they’re opencloud-compose_opencloud-config and opencloud-compose_opencloud-data; docker volume ls --filter name=opencloud confirms yours.
You can skip the third volume, collabora-proof-key. The stack generates a new key when it is missing. The tar command has to keep extended attributes, because that’s where OpenCloud stores each file’s ID, name, parent and checksums. The Debian image’s GNU tar handles that with two flags:
cd ~/opencloud-compose
mkdir -p ~/opencloud-backups
docker compose stop
docker run --rm \
-v opencloud-compose_opencloud-config:/backup/config:ro \
-v opencloud-compose_opencloud-data:/backup/data:ro \
-v ~/opencloud-backups:/out \
debian:trixie-slim \
tar --xattrs --xattrs-include='*' -czf /out/opencloud-$(date +%F-%H%M).tar.gz -C /backup config data
cp .env ~/opencloud-backups/env-$(date +%F-%H%M)
docker compose start
Our first attempt used the alpine image, and that backup looked fine at 21 MB. After a restore, though, every file returned 404 and OpenCloud logged got empty parent attribs even after assimilating for each one, because BusyBox tar had silently dropped the user.oc.* attributes. With the Debian image and the flags above, the same drill came back intact.
Restoring onto a server that has lost the volumes, or onto a new one with the same .env, is the reverse. This is the sequence we ran after deleting both volumes with docker compose down -v:
cd ~/opencloud-compose
docker compose down
BACKUP=$(ls -1t ~/opencloud-backups/opencloud-*.tar.gz | head -1)
docker volume create opencloud-compose_opencloud-config
docker volume create opencloud-compose_opencloud-data
docker run --rm \
-v opencloud-compose_opencloud-config:/backup/config \
-v opencloud-compose_opencloud-data:/backup/data \
-v ~/opencloud-backups:/in:ro \
debian:trixie-slim \
tar --xattrs --xattrs-include='*' -xzpf "/in/$(basename "${BACKUP}")" -C /backup
docker compose up -d
Because the two volumes were created by hand rather than by Compose, docker compose up may warn that they already exist and were not created by Compose. That is expected after a restore, and Compose uses them as they are.
After the restore, a WebDAV listing showed the folder and all three test files, and the text file read back with its original content. Search found them by name without a reindex, because the search index lives inside the data volume. On a real server, copy ~/opencloud-backups off the host, and keep certs/acme.json with it if you rebuild often. Web extensions such as Excalidraw live in config/opencloud/apps in the project directory, not in the volumes, so on a new server rerun the extension install step after the restore.
Upgrading OpenCloud from 7.x to 8.x
An upgrade within opencloud-compose is a git pull that moves the image tag forward, plus one migration step when you cross from 7.x to 8.x: the search index. We tested that jump on Debian 13 by checking out the compose commit that shipped 7.5.0, uploading test files, then moving to the current main branch. Run the steps in this order:
- Take the backup from the previous section.
- Run
git pullin the project directory. - Run
docker compose pull, thendocker compose up -d. - Confirm the new version on
status.php. - Rebuild the search index.
- Check search, then delete the old index.
The pull and restart took about 80 seconds, most of it image downloads, and our .env from the 7.5 checkout, on a stack without Collabora, started under the new compose files without edits. That .env predates the Yjs service, though, so add yjs/yjs.yml to your COMPOSE_FILE line after the pull if you want live editing and whiteboard sync; OpenCloud’s upgrade notes say Yjs has to be enabled explicitly. Search, however, came back empty for files that had been findable a minute earlier, and the OpenCloud log explained why:
created a new empty search index; if this OpenCloud instance already held files, they are not in it yet, index them by running: opencloud search index --all-spaces --force-rescan --insecure
The 8.0 release changed the search mapping, so it starts a new versioned index in bleve-v5 beside the old bleve directory instead of migrating it. Rebuild it with the command the log suggests, run inside the container:
docker compose exec opencloud opencloud search index --all-spaces --force-rescan --insecure
The --insecure flag refers to the internal gRPC connection, which runs without TLS between OpenCloud’s own services by default, not to your public certificate. On our small test space, the rebuild took 19 seconds:

Search matched the files again straight after, and a plain word now found them where 7.5 had needed a wildcard. Once you’ve confirmed results, remove the old index to free the space:
docker compose exec opencloud rm -r /var/lib/opencloud/search/bleve
OpenCloud’s own 8.x upgrade notes describe the same reindex and live under the “next” docs; the default docs path was still labelled 7.2 in October 2026. The 8.0.1 and 8.1.0 release notes add no further migration step. OpenCloud’s general upgrade guide also runs opencloud init --diff in a throwaway container against the config volume and applies the resulting patch to opencloud.yaml. Start that container without the admin password variables, because inside the running stack the command stops with diff and admin-password flags are mutually exclusive. We ran that diff on our 8.1 server. It only proposed flipping TLS-skip settings left by a first start we had made with INSECURE=true, so read the patch before you apply it.
If you are still deciding between OpenCloud and the alternatives, our Nextcloud, ownCloud, Seafile and Syncthing comparison and the Cloudreve, Seafile and FileBrowser roundup cover the trade-offs. The Nextcloud install on Ubuntu 26.04 is the closest like-for-like if your team depends on Nextcloud apps, since OpenCloud has no app store of that size. If raw sync speed matters more than web editing, Seafile in Docker is the other self-hosted option to look at.
Fixing the errors we hit in the lab
Every error below came from our test servers, with the fix that cleared it.
Error: “got empty parent attribs even after assimilating”
OpenCloud logs this for each file after a restore from a backup that lost extended attributes, and the WebDAV listing for the folder returns 404. Restore again from a backup made with GNU tar and --xattrs --xattrs-include='*', as in the backup section. A backup made without those flags can’t be repaired, so test a restore before you need one.
Error: “Error contacting the requested application”
The web UI showed this when we created a document and replaced the whole suggested name, extension included. OpenCloud picks the editor by file type, so a file named change-window-plan has no app provider, and the log says error finding app providers. Rename the file to end in .odt, .docx or another office extension and open it again.
Error: “libOpenGL.so.0: cannot open shared object file”
The desktop client AppImage stopped with this on a minimal Ubuntu 26.04 server install. Install libopengl0; libfuse2t64 is the other package a server image is usually missing for AppImages.
Search returns nothing after upgrading to 8.x
This is the expected state until you run opencloud search index --all-spaces --force-rescan --insecure, as covered in the upgrade section. Files, shares and sync keep working in the meantime; only search results are missing.
“E: You don’t have enough free space in /var/cache/apt/archives/”
We hit this on Debian 13 after the image pull, on a 3 GB root disk with a separate disk mounted at /var/lib/docker. Images went to /var/lib/containerd on the root filesystem and filled it. Give the root disk room for about 3 GB of images, or move containerd’s data directory to the larger disk while Docker and containerd are stopped.