Can I self-host Google Workspace?
YES · ONE COMMAND— setup effort 1 of 4YES — it's called Collabora Online. It takes one prompt, a 2048 MB VPS, and about 10 minutes. That is $42 a month you stop paying Google Workspace — $504 a year on the Business Starter plan, 5 seats assumed.
Why people pay for Google Workspace
Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.
Google Workspace arrives as one line per person and gets bought for four things at once: mail at your own domain that lands in other people's inboxes, storage everybody in the company can find, documents two people can type into at the same second without anyone merging anything afterwards, and a Meet link on the end of every calendar invite. The editing is the part with the most obvious replacement and the smallest share of the reason anyone renews. What the money is mostly buying is deliverability and an identity directory nobody has to administer, and those are the halves that stay bought.
| Plan | List price | What it buys |
|---|---|---|
| Business Starterthe plan this page prices against | $8.40/mo per seat | $7.00 per user per month on the Annual/Fixed-Term plan. 30 GB pooled storage per user, Meet up to 100 participants, capped at 300 users. |
| Business Standard | $16.80/mo per seat | $14.00 per user per month on the Annual/Fixed-Term plan. 2 TB pooled per user, Meet up to 150 participants with recording, capped at 300 users. |
| Business Plus | $26.40/mo per seat | $22.00 per user per month on the Annual/Fixed-Term plan. 5 TB pooled per user, Meet up to 500 participants, Vault and eDiscovery, capped at 300 users. |
| Enterprise | quote only | Quote only. The pricing page says contact sales, and this is the tier with no 300-user cap. |
Vendor list prices in USD, read from the pricing page on 2026-08-07 · confidence: medium
Replaced by Collabora Online
One project, named before the prompt, so you know what you are about to install.
Writer, Calc and Impress in a browser tab, on the LibreOffice engine, driven by the file app you already run.
The LibreOffice lineage, which gives it the deepest ODF handling of anything here and an office suite's worth of features rather than an approximation of one. It replaces the editing third of the subscription and nothing else: no mailbox, no storage, no interface of its own, so it needs a file application in front of it, and the pairing upstream documents is Nextcloud with the Collabora Online connector. The free CODE edition is a single container with a single generated password, which makes it the shortest path from a Docs tab to something you run yourself. Choose it when the documents that matter are yours and your colleagues', written where you write them, rather than files arriving from somebody else's Office.
What else we looked at
Ranked, with the reason each one placed where it did. Only the pick has a prompt on this page — the runners-up are named so the choice is visible, not hidden.
- Collabora Onlineour pickONE COMMAND— setup effort 1 of 4
Writer, Calc and Impress in a browser tab, on the LibreOffice engine, driven by the file app you already run.
The LibreOffice lineage, which gives it the deepest ODF handling of anything here and an office suite's worth of features rather than an approximation of one. It replaces the editing third of the subscription and nothing else: no mailbox, no storage, no interface of its own, so it needs a file application in front of it, and the pairing upstream documents is Nextcloud with the Collabora Online connector. The free CODE edition is a single container with a single generated password, which makes it the shortest path from a Docs tab to something you run yourself. Choose it when the documents that matter are yours and your colleagues', written where you write them, rather than files arriving from somebody else's Office.
- ONLYOFFICE DocsONE COMMAND— setup effort 1 of 4
The Word, Excel and PowerPoint half of an office subscription, as a server your own file app hands documents to.
The OOXML-native sibling one page over, and the safer engine when docx, xlsx and pptx round-tripping with Microsoft Office users is the daily reality, because it treats those as its own formats rather than importing and exporting them. It ranks second here only because a Workspace reader's documents usually begin life in Google's editors rather than in Word, which is exactly the ground where Collabora's filters are level and its ODF story is better. Same shape otherwise: an editing engine with no storage, needing a file application in front of it.
The swap
You'd run
Collabora Online
ONE COMMAND · ~10 min to running · 2048 MB RAM
Google Workspace Business Starter · 5 seats assumed · vendor list price · checked 2026-08-07 · source · confidence: medium
Before you start
- RAM floor
- 2048 MBfloor from upstream docs — not measured by us yet
- Disk
- 10 GBthe app, its data, and room for one backup
- Domain needed
- yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
- Time budget
- ~10 minunder 10 minutes, through the first backup
The prompt
Two paths to the same Collabora Online: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.
Where it runs
314 lines · 14,858 bytes
What this prompt will do
- Preflight
- Layout
- Secrets
- compose.yml
- Caddy and TLS
- Firewall
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.
Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.
Install Collabora Online Development Edition 26.04.2.4.1 on that server, reachable at
https://<DOMAIN>, behind the existing Caddy with automatic TLS.
## 1. Preflight
Say this to the user before anything installs, because it decides whether they want this at
all. Collabora Online is the editing engine, not a place to keep files. On its own it edits
nothing: it opens, renders and saves documents another application hands it. Install it if the
user already runs Nextcloud, or another application with a Collabora connector, or is about to.
Otherwise this leaves them a correctly running server with nothing pointed at it.
If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer.
Its A record must already point at this server, and it has to resolve for the other
application's own server as well as for every user's browser, because both talk to it.
This install needs 2048 MB of RAM available and 10 GB free on /srv. The image is about 470 MB
compressed and unpacks to several times that: it carries a whole office suite, every dictionary
and forty language packs. It publishes amd64 and arm64. Measure everything first:
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```
If available RAM is under 2048 MB or free disk is under 10 GB, print both numbers and stop. Do
not install and hope: each open document is a separate process here, so a machine short of
memory fails on the third document, not at startup. If `dig +short` prints nothing, print that
and stop.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/collabora /srv/collabora/backups
ls -la /srv/collabora
```
Assert: `ls -la` shows `backups` owned by the login user. There is no `data` directory and no
volume in step 4, because there is nothing here to keep: coolwsd builds its chroot jails and
its cache inside the container, and the upstream source turns fsync off there with the comment
that this is a state-less container. Two files are the whole install.
## 3. Secrets
One secret: the admin console password. Generate it on the server. Do not print it, do not
repeat it in your summary, and do not put it in any log line. Hex rather than base64, because
the user types this into a browser login box and hex has no characters that invite a typo.
```bash
umask 077
cat > /srv/collabora/.env <<EOF
username=admin
password=$(openssl rand -hex 24)
server_name=<DOMAIN>:443
aliasgroup1=
EOF
chmod 600 /srv/collabora/.env
umask 022
ls -l /srv/collabora/.env
```
Assert: the file exists with mode `-rw-------`. The four names are lowercase because coolwsd
reads them from the environment exactly as written. `username` and `password` become the admin
console credentials. `server_name` is what upstream documents for a server behind a reverse
proxy, so the editor stops guessing its own address from each request. `aliasgroup1` is
deliberately empty and step 7 fills it in: set at all, it switches the WOPI host list into
group mode, and with no group in it upstream's own log line reads
`all WOPI hosts will be denied`. That is the right way round, because the alternative default
lets the first application that connects claim the server, which is a race, not a policy. Tell
the user they can read the password with `sudo grep password /srv/collabora/.env`, and that
step 7 needs it.
## 4. compose.yml
```bash
cat > /srv/collabora/compose.yml <<'EOF'
# Collabora Online (CODE) · the deterministic fallback. Authored by
# caniselfhostit from the upstream sources, not copied from a repository:
# image build ........ https://github.com/CollaboraOnline/online.mirror/blob/main/docker/from-packages/Dockerfile
# env var handling ... https://github.com/CollaboraOnline/online.mirror/blob/main/wsd/COOLWSD.cpp
# config reference ... https://github.com/CollaboraOnline/online.mirror/blob/main/coolwsd.xml.in
#
# One service, and no second one hiding inside it. No database, no message
# broker, and no volume: coolwsd builds its jails and its cache inside the
# container, and the source turns fsync off there with the comment that this is
# a state-less container. Your documents live in whichever application hands
# them to this server, not here.
#
# No healthcheck is declared here, on purpose. The 26.04 image is built on a
# distroless base with no shell, so a CMD-SHELL probe of ours would have
# nothing to run it. It ships `coolwsd --probe --use-env-vars` instead.
#
# Tag and digest read from the registry on 2026-08-07; amd64 and arm64 both
# published.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
collabora:
image: collabora/code:26.04.2.4.1@sha256:1f864ce3f0c49e867787b6dd303bd6ba989542d3023f6809df558eafd04c1b97
container_name: collabora
restart: unless-stopped
env_file: /srv/collabora/.env
environment:
# Caddy terminates TLS on this box, so coolwsd serves plain http on its
# own socket and is told the client reached it over https. With
# ssl.enable false the self-signed certificate branch never runs at all.
extra_params: "--o:ssl.enable=false --o:ssl.termination=true"
ports:
# Loopback only: the host's Caddy is the only thing that reaches 8165.
- "127.0.0.1:8165:9980"
# SIGTERM has to leave coolwsd time to save and upload whatever is still
# open in an editor. Upstream's own deployment allows the same 60 seconds.
stop_grace_period: 60s
EOF
cd /srv/collabora && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`. The container listens on plain http 9980 inside, published
only on 127.0.0.1:8165.
## 5. Caddy and TLS
Append the block below to the Caddyfile Prompt Zero installed, with `<DOMAIN>` replaced by the
real hostname. Copy the file first: a syntax error here takes down every other site on the box.
```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-collabora
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Collabora Online (CODE) · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/CollaboraOnline/online.mirror/blob/main/coolwsd.xml.in and
# https://caddyserver.com/docs/caddyfile/directives/reverse_proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. That hostname is
# also server_name in .env, and it is the address you type into the application
# that uses this editor, so it must resolve for that application's server too.
<DOMAIN> {
# The editors are tens of megabytes of JavaScript, served by coolwsd's
# own file server rather than by anybody's CDN.
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
-Server
}
# There is deliberately no X-Frame-Options and no frame-ancestors rule.
# This whole product is an iframe: the application that owns the document
# embeds the editor inside its own page, and a frame-blocking header
# would leave the user looking at an empty box. The WOPI alias group in
# .env, not the browser, decides which application may load a document.
# reverse_proxy sets X-Forwarded-Proto and X-Forwarded-Host on the way
# through, which is how coolwsd builds https URLs while speaking plain
# http here, and it upgrades WebSocket connections with no extra
# configuration. Every keystroke in a shared document is a WebSocket
# frame. 8165 is the loopback port compose publishes; it is not open in
# the firewall.
reverse_proxy 127.0.0.1:8165
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
Assert: `caddy validate` exits 0 and the reload exits 0. If validate fails, restore
/etc/caddy/Caddyfile.before-collabora, reload, and report what it objected to. Caddy requests
the certificate on the first request and renews it itself, so there is nothing to schedule.
## 6. Firewall
Two ports open, both Caddy's. Idempotent, so on a box Prompt Zero configured they change
nothing:
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
80/tcp redirects to HTTPS and answers the ACME challenge, 443/tcp is the only way in, 443/udp
is HTTP/3. 8165 stays closed: it is bound to 127.0.0.1 and Caddy reaches it over loopback.
Assert: `ufw status verbose` prints `Status: active`, shows 80, 443/tcp and 443/udp, and no
rule mentioning 8165.
## 7. Start and verify
The image is about 470 MB compressed, so the pull takes a few minutes, and the first start
scans the fonts and dictionaries before anything answers.
```bash
cd /srv/collabora
docker compose pull
docker compose up -d
for i in $(seq 1 30); do body=$(curl -sS https://<DOMAIN>/ || true); echo "$i $body"; [ "$body" = "OK" ] && break; sleep 10; done
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/hosting/discovery
curl -sS https://<DOMAIN>/hosting/discovery | grep -c 'wopi-discovery'
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/browser/dist/admin/admin.html
```
Assert, all four, and print what you received for each. The loop ends printing `OK`, the plain
text body upstream's own readiness probe reads from `/`. The discovery request returns `200`.
The grep prints at least `1`. The admin console returns `401`, and that is the security assert:
the console is reachable and refuses anyone without the password from step 3. If any of the
four misses, stop, run `docker compose logs --tail 40 collabora`, and name the likely earlier
step. A `502` from Caddy in the first minutes is a container still starting; a `200` from the
admin console means .env never reached it and the console is open to the internet, which is a
stop-everything result.
The first screen is https://<DOMAIN>/hosting/discovery, an XML document whose opening element
reads `<wopi-discovery>`. The admin console at
https://<DOMAIN>/browser/dist/admin/admin.html asks for the username `admin` and the password
from step 3, and its dashboard heading then reads `Dashboard`. A running container is not
success.
STOP: tell the user nothing is editing a document yet, ask them for the address of the
application that will use this editor, and wait. Do not continue until they answer, or say they
will connect it later. If they answer, put that address in the alias group and restart:
```bash
sed -i 's|^aliasgroup1=$|aliasgroup1=https://cloud.example.com:443|' /srv/collabora/.env
cd /srv/collabora && docker compose up -d --force-recreate
sleep 20
grep -c '^aliasgroup1=https' /srv/collabora/.env
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/hosting/discovery
```
Replace `cloud.example.com` with the hostname the user gave, keeping the scheme and the `:443`.
Assert: the grep prints `1` and the discovery request prints `200`. Until that line is filled
in, this server denies every application that asks it for a document, which is a safe state
rather than a fault. Then hand the user these steps for Nextcloud: install the app named
`Collabora Online` from Apps, open `/settings/admin/richdocuments`, put `https://<DOMAIN>` in
the Collabora Online server field, and save. Nextcloud keeps that as the `wopi_url` setting and
fetches `/hosting/discovery` from it at once.
## 8. First backup and restore
One archive, and it is small, because there is no data here: the documents are in the other
application and the container keeps nothing. What is irreplaceable is the pair of files that
rebuild this service, the password and alias group in `.env` and the pinned digest in
`compose.yml`.
```bash
cd /srv/collabora
sudo tar -czf /srv/collabora/backups/collabora-config-$(date +%F).tar.gz -C /srv/collabora compose.yml .env -C /etc/caddy Caddyfile
ls -lh /srv/collabora/backups/
```
Assert: the archive exists and is non-empty. Print its size. Nothing is stopped. A backup on
the same disk as the data is not a backup, so run this from the user's machine, not the server:
```bash
mkdir -p ~/backups/collabora
scp vps:/srv/collabora/backups/*.tar.gz ~/backups/collabora/
```
To restore on a fresh box: recreate the directories as in step 2, untar the archive into
/srv/collabora, put the `Caddyfile` member at /etc/caddy with `<DOMAIN>` substituted, reload
Caddy, then `docker compose up -d` and re-run step 7's four asserts. Tell the user that is the
whole disaster plan, and that the member that matters is `.env`.
## 9. Updating later
New tags are listed at https://hub.docker.com/r/collabora/code/tags and what changed is at
https://www.collaboraonline.com/code-26-04-release-notes/. Back up first, then edit the image
line in /srv/collabora/compose.yml to the new tag and its digest:
```bash
cd /srv/collabora
docker compose pull
docker compose up -d
docker compose logs --tail 30 collabora
```
Watch that log until it settles, then re-run step 7's four asserts before calling the update
done. A major version changes the editor bundle the connected application loads, so open one
real document afterwards as well.
## 10. What will probably go wrong
The connection to Nextcloud will fail with a message that says nothing useful. I saved the
server address there, got `Collabora Online is not reachable`, and spent twenty minutes on
Caddy and DNS, both of which were fine. The editor was refusing on purpose: the alias group in
`.env` was still empty, so every WOPI host was denied, and that refusal happens deep enough
that the connector only reports a generic failure. If a connector cannot reach a server whose
`/hosting/discovery` answers `200` in your own browser, read
`docker compose logs --tail 40 collabora` and look for the alias-group line first.
## 11. Out of scope
- Do not set `DONT_GEN_SSL_CERT` or `cert_domain`. This install runs with `ssl.enable=false`
behind Caddy, so the self-signed certificate branch never runs and neither variable has
anything to do.
- Do not delete the `aliasgroup1` line to make a connector work. Removing it hands the server
to whichever application connects first, and on a public hostname that is a race with
strangers.
- Do not set `remoteconfigurl`. It points this server at a URL it fetches configuration from on
every restart, which hands the install to whoever controls that URL.
- Do not install Nextcloud here. This prompt installs the editor; the application in front of
it is a separate install with its own hostname.No terminal agent? Use the chat fallback — slower, you paste the commands
For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.
This path is slower: you paste every command yourself, and there is nobody watching the output
but you. If you can run Claude Code, use the other tab.
You are installing Collabora Online Development Edition 26.04.2.4.1 on a VPS where Prompt Zero
is done: `ssh vps` works, Docker and Caddy are installed, the firewall is default-deny. Run
everything over `ssh vps` unless a step says otherwise, and replace `<DOMAIN>` with the hostname
whose A record already points at the box.
Read this before step 1, because it decides whether you want the install at all. Collabora
Online is the editing engine, not a place to keep files. On its own it edits nothing: it opens,
renders and saves documents that another application hands to it. It is worth installing if you
already run Nextcloud, or another application with a Collabora connector, or are about to. If
you have none of those, what you will have at the end is a correctly running server with
nothing pointed at it.
## 1. Preflight
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```
You should see: at least `2048` MB available, at least `10` G free, `amd64` or `arm64`, and
your server's IP on the last line.
If you do not: an empty last line means the A record does not exist yet. Add it, wait a minute,
run `dig +short <DOMAIN>` again, because Caddy cannot get a certificate for a hostname that
does not resolve and failed attempts count against a rate limit you cannot see. Under 2048 MB
of memory, resize the box before you go on: every open document here is a separate process, so
a machine that is short of memory does not fail at startup, it fails on the third document,
which looks like a bug in the editor rather than a bug in the shopping. That hostname also has
to resolve for the other application's own server, not only for your browser, so a split-horizon
DNS setup that answers differently inside your network will bite you at step 7.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/collabora /srv/collabora/backups
ls -la /srv/collabora
```
You should see: one directory, `backups`, owned by you.
If you do not: `Permission denied` means you are not in the sudoers group, which Prompt Zero
set up. There is no `data` directory here and no volume in step 4, because there is nothing to
keep: coolwsd builds its chroot jails and its cache inside the container, and the upstream
source turns fsync off there with the comment that this is a state-less container. Two files
under this directory are the whole install.
## 3. Secrets
One secret: the admin console password. It is generated here, on the server, straight into a
file only you can read. Hex rather than base64, because you type this into a browser login box
and hex has no characters that invite a typo.
```bash
umask 077
cat > /srv/collabora/.env <<EOF
username=admin
password=$(openssl rand -hex 24)
server_name=<DOMAIN>:443
aliasgroup1=
EOF
chmod 600 /srv/collabora/.env
umask 022
ls -l /srv/collabora/.env
```
You should see: mode `-rw-------`, your own username twice, and the path. Replace `<DOMAIN>` on
the third line with your real hostname before you paste.
If you do not: a mode of `-rw-r--r--` means `umask 077` did not take effect, which happens if
you pasted the lines separately in different shells. Run `chmod 600 /srv/collabora/.env` and
carry on. If the file already existed from an earlier attempt, this block has now replaced the
password, and the admin console will only accept the new one.
Do not paste that file, the password, or any output containing it into this chat window. Read
it once at step 7 with `sudo grep password /srv/collabora/.env`, put it in your password
manager, and keep it out of anything you are typing to a chatbot.
Those four names are lowercase because coolwsd reads them from the environment exactly as
written. `username` and `password` become the admin console credentials. `server_name` is what
upstream documents for a server behind a reverse proxy, so the editor stops guessing its own
address from each request. `aliasgroup1` is deliberately empty and step 7 fills it in: set at
all, it switches the WOPI host list into group mode, and with no group in it upstream's own log
line for that state reads `all WOPI hosts will be denied`. That is the right way round. The
alternative default lets the first application that connects claim your server, which is a
race, not a policy.
## 4. compose.yml
Paste the whole block at once, including the last two lines.
```bash
cat > /srv/collabora/compose.yml <<'EOF'
# Collabora Online (CODE) · the deterministic fallback. Authored by
# caniselfhostit from the upstream sources, not copied from a repository:
# image build ........ https://github.com/CollaboraOnline/online.mirror/blob/main/docker/from-packages/Dockerfile
# env var handling ... https://github.com/CollaboraOnline/online.mirror/blob/main/wsd/COOLWSD.cpp
# config reference ... https://github.com/CollaboraOnline/online.mirror/blob/main/coolwsd.xml.in
#
# One service, and no second one hiding inside it. No database, no message
# broker, and no volume: coolwsd builds its jails and its cache inside the
# container, and the source turns fsync off there with the comment that this is
# a state-less container. Your documents live in whichever application hands
# them to this server, not here.
#
# No healthcheck is declared here, on purpose. The 26.04 image is built on a
# distroless base with no shell, so a CMD-SHELL probe of ours would have
# nothing to run it. It ships `coolwsd --probe --use-env-vars` instead.
#
# Tag and digest read from the registry on 2026-08-07; amd64 and arm64 both
# published.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
collabora:
image: collabora/code:26.04.2.4.1@sha256:1f864ce3f0c49e867787b6dd303bd6ba989542d3023f6809df558eafd04c1b97
container_name: collabora
restart: unless-stopped
env_file: /srv/collabora/.env
environment:
# Caddy terminates TLS on this box, so coolwsd serves plain http on its
# own socket and is told the client reached it over https. With
# ssl.enable false the self-signed certificate branch never runs at all.
extra_params: "--o:ssl.enable=false --o:ssl.termination=true"
ports:
# Loopback only: the host's Caddy is the only thing that reaches 8165.
- "127.0.0.1:8165:9980"
# SIGTERM has to leave coolwsd time to save and upload whatever is still
# open in an editor. Upstream's own deployment allows the same 60 seconds.
stop_grace_period: 60s
EOF
cd /srv/collabora && docker compose config >/dev/null && echo "compose OK"
```
You should see: `compose OK` and nothing else.
If you do not: `env file /srv/collabora/.env not found` means step 3 did not write the file.
`services must be a mapping` means the indentation was lost between the page and your terminal:
run `rm /srv/collabora/compose.yml` and paste again in one go. The container listens on plain
http port 9980 inside and is published only on 127.0.0.1:8165, because Caddy on the host
terminates TLS. There is no volume in that file and that is not an omission: your documents
live in the application that hands them over, and this container keeps nothing of its own.
## 5. Caddy and TLS
This appends one site block to the Caddy config Prompt Zero installed. Replace `<DOMAIN>` in
the block with your hostname before you paste. The first line takes a copy, because a syntax
error here takes down every other site on the box.
```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-collabora
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Collabora Online (CODE) · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/CollaboraOnline/online.mirror/blob/main/coolwsd.xml.in and
# https://caddyserver.com/docs/caddyfile/directives/reverse_proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. That hostname is
# also server_name in .env, and it is the address you type into the application
# that uses this editor, so it must resolve for that application's server too.
<DOMAIN> {
# The editors are tens of megabytes of JavaScript, served by coolwsd's
# own file server rather than by anybody's CDN.
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
-Server
}
# There is deliberately no X-Frame-Options and no frame-ancestors rule.
# This whole product is an iframe: the application that owns the document
# embeds the editor inside its own page, and a frame-blocking header
# would leave the user looking at an empty box. The WOPI alias group in
# .env, not the browser, decides which application may load a document.
# reverse_proxy sets X-Forwarded-Proto and X-Forwarded-Host on the way
# through, which is how coolwsd builds https URLs while speaking plain
# http here, and it upgrades WebSocket connections with no extra
# configuration. Every keystroke in a shared document is a WebSocket
# frame. 8165 is the loopback port compose publishes; it is not open in
# the firewall.
reverse_proxy 127.0.0.1:8165
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
You should see: `Valid configuration` from validate, and no output at all from reload.
If you do not: run `sudo cp /etc/caddy/Caddyfile.before-collabora /etc/caddy/Caddyfile`,
reload, and paste again. The most common cause is a `<DOMAIN>` you replaced in one place and
not the other. Caddy requests the certificate on the first request and renews it on its own, so
there is nothing to schedule.
## 6. Firewall
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
You should see: `Status: active`, rules for `80/tcp`, `443/tcp` and `443/udp`, and no rule
mentioning `8165`.
If you do not: delete anything for `8165` with `sudo ufw delete allow 8165`. That port is bound
to 127.0.0.1 by the compose file, so Caddy reaches it over loopback and nothing else can reach
it at all. 80/tcp is there to redirect to HTTPS and to answer the ACME challenge, 443/tcp is
the only way in, and 443/udp is HTTP/3, which Caddy offers by default. `Status: inactive` is a
different problem: Prompt Zero left this firewall enabled, so something has turned it off
since, and `sudo ufw enable` puts it back before you go any further.
## 7. Start and verify
The image is about 470 MB compressed, so the pull takes a few minutes, and the first start
scans the fonts and dictionaries before anything answers.
```bash
cd /srv/collabora
docker compose pull
docker compose up -d
for i in $(seq 1 30); do body=$(curl -sS https://<DOMAIN>/ || true); echo "$i $body"; [ "$body" = "OK" ] && break; sleep 10; done
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/hosting/discovery
curl -sS https://<DOMAIN>/hosting/discovery | grep -c 'wopi-discovery'
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/browser/dist/admin/admin.html
```
You should see, in order: the loop reaching `OK`, then `200`, then a count of at least `1`,
then `401`.
If you do not: the `401` is the one worth understanding. The admin console is reachable and
refusing a request that carries no credentials, which means the password from step 3 reached
the container and is in force. A `200` in its place is the opposite result and a
stop-everything one: it means the console is open to anyone who finds your hostname. Check that
`.env` reached the container with `docker compose config`. If the loop never reaches `OK`, give
it the full thirty attempts before doing anything: a `502` from Caddy in the first few minutes
is a container that is still starting, not a broken install. After that,
`docker compose logs --tail 40 collabora` is the place to look.
The first screen is https://<DOMAIN>/hosting/discovery, an XML document whose opening element
reads `<wopi-discovery>`. That is the file every connector fetches first. The admin console at
https://<DOMAIN>/browser/dist/admin/admin.html asks for the username `admin` and the password
from step 3, and its dashboard heading then reads `Dashboard`. A running container is not
success; those four asserts are.
Now read the password once and put it in your password manager:
```bash
sudo grep password /srv/collabora/.env
```
You should see: one line, the variable name followed by 48 characters of hex. Do not paste that
line into this chat.
Nothing is editing a document yet, and one line still has to be filled in. Decide the address
of the application that will use this editor, then:
```bash
sed -i 's|^aliasgroup1=$|aliasgroup1=https://cloud.example.com:443|' /srv/collabora/.env
cd /srv/collabora && docker compose up -d --force-recreate
sleep 20
grep -c '^aliasgroup1=https' /srv/collabora/.env
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/hosting/discovery
```
You should see: `1`, then `200`. Replace `cloud.example.com` with your own hostname, keeping
the scheme and the `:443`.
If you do not: a `0` from the grep means the pattern did not match, usually because the line
already had a value from an earlier attempt; open the file and set it by hand. Until that line
holds a real address this server denies every application that asks it for a document, which is
a safe state to be left in rather than a fault. To connect Nextcloud: install the app named
`Collabora Online` from Apps, open the admin page at `/settings/admin/richdocuments`, put
`https://<DOMAIN>` in the Collabora Online server field, and save. Nextcloud keeps that as the
`wopi_url` setting and fetches `/hosting/discovery` from it straight away.
## 8. First backup and restore
One archive, and it is small, because there is no data here: the documents are in the other
application and the container keeps nothing. What is irreplaceable is the pair of files that
rebuild this service, the password and alias group in `.env` and the pinned digest in
`compose.yml`.
```bash
cd /srv/collabora
sudo tar -czf /srv/collabora/backups/collabora-config-$(date +%F).tar.gz -C /srv/collabora compose.yml .env -C /etc/caddy Caddyfile
ls -lh /srv/collabora/backups/
```
You should see: one file, a few kilobytes. Nothing goes offline.
If you do not: run `tar -tzf` on the finished archive to list what is inside it. `compose.yml`,
`.env` and `Caddyfile` should all be there, and if `Caddyfile` is missing then tar never
reached the second `-C` because the first path was wrong.
A backup on the same disk as the data is not a backup. Run this one on your own machine, not
the server:
```bash
mkdir -p ~/backups/collabora
scp vps:/srv/collabora/backups/*.tar.gz ~/backups/collabora/
```
You should see: one file copied, and it listed by `ls -lh ~/backups/collabora/`.
If you do not: `Permission denied (publickey)` means you ran it on the server. The `vps:`
prefix only means something on your own machine, where the `vps` alias Prompt Zero created
lives.
Now prove the restore, today, while nothing is at stake:
```bash
cd /srv/collabora
docker compose down
sudo rm -f /srv/collabora/compose.yml /srv/collabora/.env
sudo tar -xzf /srv/collabora/backups/collabora-config-$(date +%F).tar.gz -C /srv/collabora --exclude Caddyfile
docker compose up -d
for i in $(seq 1 30); do body=$(curl -sS https://<DOMAIN>/ || true); echo "$i $body"; [ "$body" = "OK" ] && break; sleep 10; done
```
You should see: the loop reaching `OK` again, which means two deleted files came back and the
service came back with them.
If you do not: the archive also contains a `Caddyfile` member, and `--exclude Caddyfile` keeps
it out of /srv/collabora where it would do nothing. On a genuinely fresh box you would put that
member at /etc/caddy/Caddyfile instead, with `<DOMAIN>` already replaced, and reload Caddy.
That is the whole disaster plan: two files back in place and one `docker compose up -d`. The
member that matters is `.env`, because losing it means a new password and a connector that has
to be told about it.
## 9. Updating later
New tags are listed at https://hub.docker.com/r/collabora/code/tags and what changed is at
https://www.collaboraonline.com/code-26-04-release-notes/. Take the backup first, then edit the
`image:` line in /srv/collabora/compose.yml to the new tag and its digest.
```bash
cd /srv/collabora
docker compose pull
docker compose up -d
docker compose logs --tail 30 collabora
```
You should see: the start-up sequence, then no repeating restart.
If you do not: put the old tag and digest back and run the same three commands. Then re-run the
four asserts from step 7 before you call the update done, and open one real document in the
connected application as well, because a major version changes the editor bundle that
application loads and a server that answers `OK` can still be serving a bundle the connector
does not understand.
## 10. What will probably go wrong
The connection to Nextcloud will fail with a message that says nothing useful. I saved the
server address there, got `Collabora Online is not reachable`, and spent twenty minutes on
Caddy and DNS, both of which were fine. The editor was refusing on purpose: the alias group in
`.env` was still empty, so every WOPI host was denied, and that refusal happens deep enough
that the connector only reports a generic failure. If a connector cannot reach a server whose
`/hosting/discovery` answers `200` in your own browser, read
`docker compose logs --tail 40 collabora` and look for the alias-group line first.
## 11. Out of scope
- Do not set `DONT_GEN_SSL_CERT` or `cert_domain`. This install runs with `ssl.enable=false`
behind Caddy, so the self-signed certificate branch never runs and neither variable has
anything to do.
- Do not delete the `aliasgroup1` line to make a connector work. Removing it hands the server
to whichever application connects first, and on a public hostname that is a race with
strangers.
- Do not set `remoteconfigurl`. It points this server at a URL it fetches configuration from on
every restart, which hands the install to whoever controls that URL.
- Do not install Nextcloud here. This prompt installs the editor; the application in front of
it is a separate install with its own hostname.306 lines · 14,993 bytes
What this prompt will do
- Preflight
- Docker
- Layout
- Secrets
- compose.yml
- Nothing is public
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.
Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.
Install Collabora Online Development Edition 26.04.2.4.1 under ~/selfhost/collabora, answering
at http://localhost:8165.
## 1. Preflight
Say this before step 2 runs; it decides whether they want this install at all. Collabora Online
is an editing engine, not a place to keep files: on its own it edits nothing, it renders and
saves documents another application hands it. That application has to be on this same computer,
because the editor answers at http://localhost:8165, which means "this machine" wherever it is
read, and nobody else can open a document alongside them.
Detect the OS and measure the machine:
```bash
uname -s
case "$(uname -s)" in
Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```
`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash. On Linux the
distribution ID and codename print next, for step 2. This install needs 2048 MB of RAM
available and 10 GB free on the home disk; amd64 and arm64 are both published. On macOS and
Windows the figure printed is the host's, and Docker Desktop takes its allocation out of it. If
available RAM is under 2048 MB or free disk is under 10 GB, print both numbers and stop. Each
open document is a separate process here, so a machine short of memory fails on the third
document, not at startup.
## 2. Docker
Check before installing anything:
```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```
If that printed `docker OK` and a compose version, skip to step 3.
Otherwise, install Docker for the OS step 1 detected:
- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
Homebrew, STOP: tell the user to download Docker Desktop from
https://www.docker.com/products/docker-desktop/ and install it, and wait until they
confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
terms, and wait for the whale icon to say it is running. Do not continue until they
confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
install fails, STOP: tell the user to download Docker Desktop from the URL above and
install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
repository, with its signing key saved to a file first, never piped into a shell. The
fence is guarded, a no-op on anything but a Linux with apt:
```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker "$USER"
fi
```
Adding the user to the docker group is root-equivalent on this machine; say that to the
user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
plugin with their distribution's package manager, and to run this prompt again once
`docker info` works.
Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.
## 3. Layout
```bash
mkdir -p ~/selfhost/collabora/backups
ls -la ~/selfhost/collabora
```
Assert: `ls -la` shows `backups`, owned by the user. There is no `data` folder and no ownership
fix on any of the three systems, because step 5 declares no volume at all: coolwsd builds its
jails and its cache inside the container, which the source calls state-less. Two files are the
whole install.
## 4. Secrets
One secret: the admin console password. Generate it here, print it nowhere, and keep it out of
your summary and any log line. Hex rather than base64, because the user types this into a
browser login box and hex has no characters that invite a typo.
```bash
umask 077
cat > ~/selfhost/collabora/.env <<EOF
username=admin
password=$(openssl rand -hex 24)
server_name=localhost:8165
aliasgroup1=
EOF
chmod 600 ~/selfhost/collabora/.env
umask 022
ls -l ~/selfhost/collabora/.env
```
Assert: the file exists with mode `-rw-------`. Git Bash ships openssl, so this runs the same
on all three. The four names are lowercase because coolwsd reads them from the environment
exactly as written. `username` and `password` become the admin console credentials.
`aliasgroup1` is deliberately empty and step 7 fills it in: set at all, it switches the WOPI
host list into group mode, and with no group in it upstream's own log line reads
`all WOPI hosts will be denied`. The alternative default lets the first application that
connects claim the server. The user reads the password with `grep password .env` here.
On Windows those mode bits are advisory: NTFS does not enforce them, and the real boundary is
the user's own Windows account.
## 5. compose.yml
```bash
cat > ~/selfhost/collabora/compose.yml <<'EOF'
# Collabora Online (CODE) · the deterministic fallback for the local path.
# Authored by caniselfhostit from the upstream sources, not copied:
# image build ........ https://github.com/CollaboraOnline/online.mirror/blob/main/docker/from-packages/Dockerfile
# env var handling ... https://github.com/CollaboraOnline/online.mirror/blob/main/wsd/COOLWSD.cpp
# config reference ... https://github.com/CollaboraOnline/online.mirror/blob/main/coolwsd.xml.in
#
# One service, same tag, digest and host port as the server file, with the env
# file path relative so it works from ~/selfhost/collabora/ on all three
# systems. No volumes and no bind mounts at all: coolwsd builds its jails and
# its cache inside the container, which the source calls state-less. Your
# documents live in whichever application hands them over, not here.
#
# No healthcheck is declared here. The 26.04 image is built on a distroless
# base with no shell, so a CMD-SHELL probe of ours would have nothing to run
# it. It ships `coolwsd --probe --use-env-vars` instead, which GETs /livez.
#
# Digest read on 2026-08-07; amd64 and arm64 both published.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
collabora:
image: collabora/code:26.04.2.4.1@sha256:1f864ce3f0c49e867787b6dd303bd6ba989542d3023f6809df558eafd04c1b97
container_name: collabora
restart: unless-stopped
env_file: ./.env
environment:
# Nothing here terminates TLS, so coolwsd serves plain http and the
# links it builds say http. With ssl.enable false the self-signed
# certificate branch never runs, so no throwaway certificate is made.
extra_params: "--o:ssl.enable=false"
ports:
# Loopback only: no other device on the wifi can reach 8165.
- "127.0.0.1:8165:9980"
# SIGTERM has to leave coolwsd time to save and upload whatever is still
# open in an editor. Upstream's own deployment allows the same 60 seconds.
stop_grace_period: 60s
EOF
cd ~/selfhost/collabora && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`: one service, one published port, no volumes.
## 6. Nothing is public
No reverse proxy, no certificate, no firewall rule. Each is a decision:
- No DNS. There is no hostname, so nothing to resolve and nothing to wait for.
- No TLS. A certificate attests a public name and nothing here has one. Browsers treat
http://localhost as a secure context, so pages needing crypto still work.
- No firewall rule. Nothing is published beyond loopback, so no port needs shutting.
8165 is bound to 127.0.0.1: this computer, not the user's phone, not a laptop on the same wifi.
That is the point of this path, not a defect in it. Confirm it:
```bash
grep -c '"127.0.0.1:' ~/selfhost/collabora/compose.yml
```
Assert: that prints `1`, the published-port line. Nothing else is published.
## 7. Start and verify
The image is about 470 MB compressed, so the pull takes minutes, and the first start scans the
fonts and dictionaries before anything answers.
```bash
cd ~/selfhost/collabora
docker compose pull
docker compose up -d
for i in $(seq 1 30); do body=$(curl -sS http://localhost:8165/ || true); echo "$i $body"; [ "$body" = "OK" ] && break; sleep 10; done
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8165/hosting/discovery
curl -sS http://localhost:8165/hosting/discovery | grep -c 'wopi-discovery'
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8165/browser/dist/admin/admin.html
```
Assert all four, and print what you received for each: the loop ends on `OK`, the plain text
body upstream's own readiness probe reads from `/`; discovery returns `200`; the grep prints at
least `1`; the admin console returns `401`, the security assert, because it proves the console
refuses anyone without the password from step 4. If any of the four misses, stop, run
`docker compose logs --tail 40 collabora`, and name the cause: an empty reply in the first
minutes is a container still starting, and a `200` from the console means .env never reached
it. If `port is already allocated` came back, find what holds 8165
(`lsof -nP -iTCP:8165 -sTCP:LISTEN`, or `netstat -ano | findstr :8165` on Windows) and stop
until the user frees it. A running container is not success.
The first screen is http://localhost:8165/hosting/discovery, an XML document whose opening
element reads `<wopi-discovery>`. The admin console at
http://localhost:8165/browser/dist/admin/admin.html asks for the username `admin` and the
step 4 password, and its dashboard heading then reads `Dashboard`.
STOP: tell the user nothing is editing a document yet, ask for the address their Nextcloud
answers on, and wait. Do not continue until they answer, or say they will connect it later.
Then put that address in the alias group and restart:
```bash
sed -i.bak 's|^aliasgroup1=$|aliasgroup1=http://localhost:8080|' ~/selfhost/collabora/.env
cd ~/selfhost/collabora && docker compose up -d --force-recreate
sleep 20
grep -c '^aliasgroup1=http' ~/selfhost/collabora/.env
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8165/hosting/discovery
```
Replace `http://localhost:8080` with the address the user gave, scheme and port included, then
`rm ~/selfhost/collabora/.env.bak`. Assert: the grep prints `1` and discovery prints `200`.
Until then this server denies every application that asks for a document, a safe state rather
than a fault. The user's Nextcloud steps: install the app named `Collabora Online` from Apps,
open `/settings/admin/richdocuments`, put `http://localhost:8165` in the server field, save.
Read them step 10 first: that address is where this goes wrong.
## 8. First backup and restore
One archive of two files, and it is the whole install: the documents are in the other
application and this container keeps nothing. What is irreplaceable is the password and alias
group in `.env`, and the pinned digest in `compose.yml`.
```bash
cd ~/selfhost/collabora
tar -czf backups/collabora-config-$(date +%F).tar.gz compose.yml .env
ls -lh backups/
```
Assert: the archive exists and is non-empty. Print its size. Nothing is stopped.
That archive sits on the same disk as everything else, which is not a backup: on a laptop the
disk and the machine fail together. Ask the user for a destination that leaves this computer, a
folder their sync service watches or a USB stick, and copy it there with `cp`. In Git Bash a
Windows drive is written `/d/Backups`, not `D:\Backups`. Assert: the user confirms the filename
is listed there; if they have neither, say plainly this install has no backup.
Prove the restore now, while nothing is at stake. In `~/selfhost/collabora`: `docker compose
down`, `rm -f compose.yml .env`, `tar -xzf backups/collabora-config-$(date +%F).tar.gz`,
`docker compose up -d`, then re-run step 7's loop. Assert: it ends on `OK` again. That is the
whole disaster plan, and anything open in an editor at that moment is lost, though the document
itself is not.
## 9. Updating later
New tags are at https://hub.docker.com/r/collabora/code/tags and what changed is at
https://www.collaboraonline.com/code-26-04-release-notes/. Back up first, then edit the image
line in ~/selfhost/collabora/compose.yml to the new tag and digest:
```bash
cd ~/selfhost/collabora
docker compose pull
docker compose up -d
docker compose logs --tail 30 collabora
```
Watch that log until it settles, then re-run step 7's four asserts before calling it done.
## 10. What will probably go wrong
The address will work in the browser and fail in the application anyway. I put
http://localhost:8165 into Nextcloud's Collabora settings, watched the editor load in my own
browser, and got an error saying the server was unreachable. Both were true: my browser was on
this machine, so localhost was this machine, but Nextcloud's server fetches
`/hosting/discovery` from that address too, and if Nextcloud is in a container then localhost
is that container, where nothing listens. Run that application outside a container, or put
both on one Docker network and use the service name in the settings field and the alias group.
## 11. Out of scope
- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not delete the `aliasgroup1` line to make a connector work. Removing it hands the server
to whichever application connects first, and step 10 is the real problem it hides.
- Do not set `remoteconfigurl`. It fetches configuration from a URL on every restart, which
hands the install to whoever controls that URL.
- Do not install Nextcloud here. This prompt installs the editor.compose.local.ymlthe services, pinned · local layout37 lines
# Collabora Online (CODE) · the deterministic fallback for the local path.
# Authored by caniselfhostit from the upstream sources, not copied:
# image build ........ https://github.com/CollaboraOnline/online.mirror/blob/main/docker/from-packages/Dockerfile
# env var handling ... https://github.com/CollaboraOnline/online.mirror/blob/main/wsd/COOLWSD.cpp
# config reference ... https://github.com/CollaboraOnline/online.mirror/blob/main/coolwsd.xml.in
#
# One service, same tag, digest and host port as the server file, with the env
# file path relative so it works from ~/selfhost/collabora/ on all three
# systems. No volumes and no bind mounts at all: coolwsd builds its jails and
# its cache inside the container, which the source calls state-less. Your
# documents live in whichever application hands them over, not here.
#
# No healthcheck is declared here. The 26.04 image is built on a distroless
# base with no shell, so a CMD-SHELL probe of ours would have nothing to run
# it. It ships `coolwsd --probe --use-env-vars` instead, which GETs /livez.
#
# Digest read on 2026-08-07; amd64 and arm64 both published.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
collabora:
image: collabora/code:26.04.2.4.1@sha256:1f864ce3f0c49e867787b6dd303bd6ba989542d3023f6809df558eafd04c1b97
container_name: collabora
restart: unless-stopped
env_file: ./.env
environment:
# Nothing here terminates TLS, so coolwsd serves plain http and the
# links it builds say http. With ssl.enable false the self-signed
# certificate branch never runs, so no throwaway certificate is made.
extra_params: "--o:ssl.enable=false"
ports:
# Loopback only: no other device on the wifi can reach 8165.
- "127.0.0.1:8165:9980"
# SIGTERM has to leave coolwsd time to save and upload whatever is still
# open in an editor. Upstream's own deployment allows the same 60 seconds.
stop_grace_period: 60sagent-readable mirror: /self-host/google-workspace-business-starter.md
The files, if you'd rather do it yourself
The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.
compose.ymlthe services, pinned38 lines
# Collabora Online (CODE) · the deterministic fallback. Authored by
# caniselfhostit from the upstream sources, not copied from a repository:
# image build ........ https://github.com/CollaboraOnline/online.mirror/blob/main/docker/from-packages/Dockerfile
# env var handling ... https://github.com/CollaboraOnline/online.mirror/blob/main/wsd/COOLWSD.cpp
# config reference ... https://github.com/CollaboraOnline/online.mirror/blob/main/coolwsd.xml.in
#
# One service, and no second one hiding inside it. No database, no message
# broker, and no volume: coolwsd builds its jails and its cache inside the
# container, and the source turns fsync off there with the comment that this is
# a state-less container. Your documents live in whichever application hands
# them to this server, not here.
#
# No healthcheck is declared here, on purpose. The 26.04 image is built on a
# distroless base with no shell, so a CMD-SHELL probe of ours would have
# nothing to run it. It ships `coolwsd --probe --use-env-vars` instead.
#
# Tag and digest read from the registry on 2026-08-07; amd64 and arm64 both
# published.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
collabora:
image: collabora/code:26.04.2.4.1@sha256:1f864ce3f0c49e867787b6dd303bd6ba989542d3023f6809df558eafd04c1b97
container_name: collabora
restart: unless-stopped
env_file: /srv/collabora/.env
environment:
# Caddy terminates TLS on this box, so coolwsd serves plain http on its
# own socket and is told the client reached it over https. With
# ssl.enable false the self-signed certificate branch never runs at all.
extra_params: "--o:ssl.enable=false --o:ssl.termination=true"
ports:
# Loopback only: the host's Caddy is the only thing that reaches 8165.
- "127.0.0.1:8165:9980"
# SIGTERM has to leave coolwsd time to save and upload whatever is still
# open in an editor. Upstream's own deployment allows the same 60 seconds.
stop_grace_period: 60sCaddyfilethe hostname and TLS37 lines
# Collabora Online (CODE) · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/CollaboraOnline/online.mirror/blob/main/coolwsd.xml.in and
# https://caddyserver.com/docs/caddyfile/directives/reverse_proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. That hostname is
# also server_name in .env, and it is the address you type into the application
# that uses this editor, so it must resolve for that application's server too.
<DOMAIN> {
# The editors are tens of megabytes of JavaScript, served by coolwsd's
# own file server rather than by anybody's CDN.
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
-Server
}
# There is deliberately no X-Frame-Options and no frame-ancestors rule.
# This whole product is an iframe: the application that owns the document
# embeds the editor inside its own page, and a frame-blocking header
# would leave the user looking at an empty box. The WOPI alias group in
# .env, not the browser, decides which application may load a document.
# reverse_proxy sets X-Forwarded-Proto and X-Forwarded-Host on the way
# through, which is how coolwsd builds https URLs while speaking plain
# http here, and it upgrades WebSocket connections with no extra
# configuration. Every keystroke in a shared document is a WebSocket
# frame. 8165 is the loopback port compose publishes; it is not open in
# the firewall.
reverse_proxy 127.0.0.1:8165
}install.shthe same install, no agent178 lines
#!/usr/bin/env bash
# Collabora Online (CODE) · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
# DOMAIN_HOST=office.example.com WOPI_HOST=https://cloud.example.com:443 ./install.sh
#
# Authored by caniselfhostit from the upstream sources:
# https://github.com/CollaboraOnline/online.mirror/blob/main/docker/from-packages/Dockerfile
# https://github.com/CollaboraOnline/online.mirror/blob/main/docker/README
# https://github.com/CollaboraOnline/online.mirror/blob/main/wsd/COOLWSD.cpp
# https://github.com/CollaboraOnline/online.mirror/blob/main/coolwsd.xml.in
#
# One secret is generated here, on this machine: the admin console password. It
# goes into /srv/collabora/.env with mode 600 and is never printed.
#
# This installs the editing engine only. It edits nothing on its own: another
# application, such as Nextcloud with the Collabora Online connector, hands it
# the documents. DOMAIN_HOST is the address that application will be pointed at,
# so it has to resolve for its server as well as for your browser. WOPI_HOST is
# the reverse: the address that application answers on, which this server will
# accept documents from. Leave WOPI_HOST unset and the alias group stays empty,
# which upstream treats as "all WOPI hosts will be denied" until you fill it in.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail
APP_DIR="${APP_DIR:-/srv/collabora}"
DOMAIN_HOST="${DOMAIN_HOST:-}"
WOPI_HOST="${WOPI_HOST:-}"
die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }
# --- 1. Refuse to start on a machine that is not ready -----------------------
[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. office.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
command -v openssl >/dev/null 2>&1 || die "openssl is not installed"
avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 2048 ] || die "only ${avail_mb} MB of RAM available; this install wants 2048 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 10 ] || die "only ${avail_gb} GB free on /srv; this install wants 10 GB"
resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}' || true)"
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."
case "${WOPI_HOST}" in
""|http://*|https://*) : ;;
*) die "WOPI_HOST must include a scheme and a port, e.g. https://cloud.example.com:443" ;;
esac
# --- 2. Lay the files out ----------------------------------------------------
#
# No data directory and no volume: coolwsd builds its chroot jails and its cache
# inside the container, which the upstream source calls a state-less container.
sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" "$APP_DIR" "$APP_DIR/backups"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"
# --- 3. Generate the one secret, on the server -------------------------------
#
# Hex rather than base64: this value is typed into a browser login box, and hex
# has no characters that invite a typo. Read it later with
# sudo grep password /srv/collabora/.env
#
# The four names are lowercase because coolwsd reads them from the environment
# exactly as written: username and password become the admin console
# credentials, server_name is what upstream documents for a server behind a
# reverse proxy, and aliasgroup1 is the WOPI host allowlist.
if [ ! -f "$APP_DIR/.env" ]; then
umask 077
cat > "$APP_DIR/.env" <<-ENVFILE
username=admin
password=$(openssl rand -hex 24)
server_name=${DOMAIN_HOST}:443
aliasgroup1=${WOPI_HOST}
ENVFILE
chmod 600 "$APP_DIR/.env"
umask 022
fi
cd "$APP_DIR"
docker compose config >/dev/null
# --- 4. Caddy site block, on the host ----------------------------------------
if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-collabora"
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
# --- 5. Ports: two open, and 8165 is not one of them --------------------------
if command -v ufw >/dev/null 2>&1; then
echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8165 stays closed"
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
fi
# --- 6. Start it -------------------------------------------------------------
#
# The image is about 470 MB compressed and the first start scans the fonts and
# dictionaries, so a few minutes of 502 here is normal rather than a fault.
docker compose pull
docker compose up -d
echo "==> waiting for https://${DOMAIN_HOST}/"
for _ in $(seq 1 30); do
body="$(curl -sS "https://${DOMAIN_HOST}/" || true)"
[ "$body" = "OK" ] && break
sleep 10
done
[ "${body:-}" = "OK" ] || die "the root probe answered '${body:-nothing}'. Check: docker compose logs --tail 40 collabora"
discovery="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/hosting/discovery" || true)"
[ "$discovery" = "200" ] || die "/hosting/discovery answered ${discovery}, not 200"
curl -sS "https://${DOMAIN_HOST}/hosting/discovery" | grep -q 'wopi-discovery' \
|| die "/hosting/discovery did not contain the expected wopi-discovery element"
# The admin console must refuse a request that carries no credentials. Upstream
# answers 401 there when the console is enabled and the caller is not logged in;
# a 200 would mean the password never reached the container and the console is
# open to anyone who finds this hostname.
console="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/browser/dist/admin/admin.html" || true)"
[ "$console" = "401" ] || die "the admin console returned ${console}, not 401. Stop and investigate."
# --- 7. The first backup, before day one ends --------------------------------
#
# Two files rebuild this service completely, and the Caddy site block is
# archived from /etc/caddy so the copy has the real hostname in it rather than
# the <DOMAIN> template.
STAMP="$(date +%Y%m%d-%H%M%S)"
sudo tar -czf "$APP_DIR/backups/collabora-config-${STAMP}.tar.gz" -C "$APP_DIR" compose.yml .env -C /etc/caddy Caddyfile
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/collabora-config-${STAMP}.tar.gz" ] || die "the config archive is empty"
if [ -z "$WOPI_HOST" ]; then
alias_note="No WOPI_HOST was given, so aliasgroup1 is empty and this server denies every application that asks it for a document. Put the address in $APP_DIR/.env, then run: docker compose up -d --force-recreate"
else
alias_note="aliasgroup1 is ${WOPI_HOST}, so only that host may load documents through this server."
fi
cat <<-DONE
Collabora Online is answering at https://${DOMAIN_HOST}/hosting/discovery
1. Nothing is editing a document yet. This is the editing engine, and
another application has to be pointed at it. In Nextcloud: install
the app named Collabora Online, open /settings/admin/richdocuments,
put https://${DOMAIN_HOST} in the Collabora Online server field, and
save.
2. ${alias_note}
3. The admin console is at
https://${DOMAIN_HOST}/browser/dist/admin/admin.html
Username admin. The password is in $APP_DIR/.env, mode 600. Read it
with
sudo grep password $APP_DIR/.env
and put it in your password manager. It was not printed here. An
unauthenticated request to that page was refused with 401, which is
the check that proves the password is in force.
4. First backup written to $APP_DIR/backups. It is on the same disk as
the config, which is not a backup. Copy it somewhere else tonight.
DONEWhat you're signing up for
The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel Google Workspace.
- This replaces one part of the subscription, not the subscription. You get Writer, Calc and Impress in a browser tab, co-editing included, on the LibreOffice engine and its native ODF formats. You do not get Gmail, the pooled Drive storage, or Meet. The mail and the storage are separate problems, and the storage half upstream documents is Nextcloud with the Collabora Online connector app.
- On its own it edits nothing. A document server with no application in front of it is a correctly running server with an empty queue: it opens, renders and saves documents that something else hands it, and that something else is a separate install with its own hostname and its own backups.
- One list decides who may use it. The WOPI alias group in .env names the applications allowed to load documents through this server, and leaving it unset does not mean 'open to me', it means whichever application connects first claims the server. This install sets that list deliberately and asserts it.
- Two gigabytes of RAM and about 470 MB to download before it starts, because this is an office suite compiled to run on a server, with every dictionary and forty language packs inside the image. Each open document is a separate process on top of that.
- The image is CODE, the free edition upstream describes as aimed at home users and small teams. MPL-2.0 throughout, with no support contract, no clustering, and a welcome screen and feedback popup you cannot switch off unless you turn on home mode and accept its cap of 20 connections and 10 open documents. Those are what the paid editions sell.
Where this came from
“The Collabora Online Development Edition (CODE) is aimed at home users and small teams.”
- Under --use-env-vars coolwsd reads username, password, server_name and aliasgroup1 from the environment and maps them onto admin_console.username, admin_console.password, server_name and the WOPI alias groups, while extra_params is appended to its command line. source
- With no alias group configured the WOPI host list runs in 'first' mode, which allows only the first host that connects; naming an alias group and leaving it empty switches it to 'groups' mode with nothing in it, and upstream logs that all WOPI hosts will be denied. source
- The admin console at /browser/dist/admin/admin.html authenticates with HTTP Basic credentials and answers 401 with a WWW-Authenticate header when none are supplied, which is what makes an unauthenticated request a usable check that the generated password reached the container. source
- Home mode limits concurrent open connections to 20 and concurrent open documents to 10, and it is off by default: the documented trade is that welcome and user-feedback popups cannot be switched off unless you accept those limits. source
- The Nextcloud connector is the richdocuments app, configured under Admin then Collabora Online, which stores the server address as the wopi_url setting and reads /hosting/discovery and /hosting/capabilities from it. source
Questions people actually ask
Answered from this page's own data — the same numbers, in sentences.
Can I self-host Google Workspace?
Not Google Workspace itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is Collabora Online. Writer, Calc and Impress in a browser tab, on the LibreOffice engine, driven by the file app you already run. The install is one command: one container behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 10 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.
What replaces Google Workspace?
Collabora Online. Writer, Calc and Impress in a browser tab, on the LibreOffice engine, driven by the file app you already run. The LibreOffice lineage, which gives it the deepest ODF handling of anything here and an office suite's worth of features rather than an approximation of one. It replaces the editing third of the subscription and nothing else: no mailbox, no storage, no interface of its own, so it needs a file application in front of it, and the pairing upstream documents is Nextcloud with the Collabora Online connector. The free CODE edition is a single container with a single generated password, which makes it the shortest path from a Docs tab to something you run yourself. Choose it when the documents that matter are yours and your colleagues', written where you write them, rather than files arriving from somebody else's Office. Also evaluated and ranked below it: ONLYOFFICE Docs. The prompt on this page installs Collabora Online only. Collabora Online is MPL-2.0-licensed and free; nothing on this page is a hosted service we sell you.
What does self-hosting cost compared to Google Workspace?
2048 MB of RAM and 10 GB of disk — the smallest tier most VPS hosts sell, about $10 a month. Collabora Online itself is free and MPL-2.0-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: Google Workspace Business Starter, $42/mo — $504 a year, 5 seats assumed.
How hard is it really?
ONE COMMAND — under 10 minutes. The rule that produced that verdict: one container, no database, no outside integration, at most one secret. Nothing to negotiate with anyone else, nothing to back up separately, at most one secret to generate. This is the case where the compose file honestly is the whole install. The tier is derived from seven countable facts about the Collabora Online install, not from anyone's impression of it, and the whole rubric is published on the methodology page.
Can I run Collabora Online on my own computer instead of a server?
Yes — that is the second path in the prompt box above. "On my computer" installs the same Collabora Online on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. The catch: The editor answers only at http://localhost:8165, so the file application that drives it has to be on this same computer, and nobody else can open a document alongside you. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.
Content last checked 2026-08-07. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.