
A four-VM cloud productivity stack: Nextcloud + Postgres + Redis + Collabora, from one prompt
March 28, 2026
Nextcloud is the most-deployed self-hosted alternative to Google Drive / Microsoft 365, with file sync, calendar, contacts, Talk, and collaborative office docs in one app. It's consistently near the top of every r/selfhosted survey and the seventh-most-deployed in 2026. Where Immich owns photos, Nextcloud owns everything else — the documents, the shared calendars, the client-facing share links, the in-browser editor your non-technical family or team actually uses.
The platform you'd deploy today is Nextcloud Hub 10 (released February 25, 2025). It's a telling release: fewer than 5% of its ~4,600 code changes were new features — the rest went into stability and performance. Headline wins include up to 6× faster chunked uploads (a 10 GB file in 45 seconds instead of 195), end-to-end encrypted Talk calls, and an on-premise AI Assistant 3.0 that can draft, translate, and even take actions like creating calendar events — all self-hosted, so your data never leaves the box.
This post walks through the production-shaped Nextcloud stack on OpenFactory: four buildable VMs — the Nextcloud app, Postgres, Redis cache, and Collabora Online for in-browser office docs — from one prompt, shipped as bootable ISOs. The recipe is a preparation lab: it lands the right topology, a ready config.php.example already pointed at every backend, and compatibility stubs that prove the wiring, so installing the real PHP app is the last step rather than a debugging marathon.
nextcloud-app (10.76.0.10:80) — the app server with a ready config.php.example wired to all three backends.nextcloud-db (10.76.0.20:5432) — Postgres configured for the Nextcloud schema with subnet ACLs in place.nextcloud-redis (10.76.0.30:6379) — Redis for memcache.local and file-lock backends; absolute requirement for multi-user Nextcloud performance.collabora (10.76.0.40:9980) — CODE host for in-browser doc / sheet / slide editing.config.php.example already points at Postgres, Redis, and Collabora — copy it to config.php at deploy and you're wired. The four hostnames that a hand-rolled install gets wrong on the first three tries are baked into the image.Four Debian Trixie VMs on 10.76.0.0/24. The app server is the only node with public-facing intent; the database, cache, and office host stay on the lab subnet, reachable only from the app. Browsers talk to nextcloud-app for files and to collabora for the live editing canvas — a WOPI handshake the app brokers behind the scenes.
Almost every “Nextcloud feels slow” thread comes down to the same three knobs, and the four-VM shape exists to make them tunable independently.
PHP-FPM handles each request in a separate worker process, so a default pool starves quickly — raising pm.max_children and enabling opcache + APCu is usually the first fix on the app VM. Redis absorbs the memory cache and the file-lock traffic that would otherwise pound the database. And Postgres, on its own VM, gets the RAM and the slow-query visibility (pg_stat_statements) it needs without the PHP workers competing for the same cores. For a hundred-plus-user deployment, community guidance lands around 16 GB of RAM minimum across the stack — another reason to separate the roles so you can grow the one that's actually the bottleneck.
Paste this verbatim into the chat builder at console.openfactory.tech. Nothing above or below it — the builder expects the prompt body to start at the “Build a compact multi-node lab…” line.
Build a compact multi-node lab named `nextcloud-cloud-stack`.
Output discipline: keep the plan small. Use one startup script per node, about 25 shell lines or less. Do not install Nextcloud server PHP code, Apache or PHP-FPM, Collabora Online CODE binaries, or external `apt` repos at build time. Write deployment-time config examples and tiny Python stdlib or shell compatibility stubs only. The goal is a buildable preparation lab, not a production deployment.
## Topology
Create 4 buildable `debian-trixie` nodes, all `x86_64`, SSH enabled, DHCP/default route intact with lab aliases, firewall disabled, DNS `1.1.1.1` and `8.8.8.8`, user `ops` password `nc-ops` in `sudo`. Every recipe must set top-level `test_config` to `{ "enabled": false, "tests": [] }`.
- `nextcloud-app`: role `cloud-app`, 3 GB RAM, 24 GB disk, alias `10.76.0.10/24`, x `230`, y `60`
- `nextcloud-db`: role `database`, 2 GB RAM, 16 GB disk, alias `10.76.0.20/24`, x `110`, y `220`
- `nextcloud-redis`: role `cache`, 1 GB RAM, 8 GB disk, alias `10.76.0.30/24`, x `350`, y `220`
- `collabora`: role `office`, 2 GB RAM, 12 GB disk, alias `10.76.0.40/24`, x `230`, y `380`
Connections: `nextcloud-app` to `nextcloud-db:5432`, `nextcloud-redis:6379`, `collabora:9980`.
## Common Recipe Requirements
All nodes: features `headless`, `ssh`; packages `openssh-server`, `python3`, `curl`, `jq`, `iproute2`, `netcat-openbsd`, `ca-certificates`. Each startup script adds the alias with `IFACE=$(ip route show default | awk '{print $5; exit}')`, `ip link set "$IFACE" up || true`, and `ip addr add <alias> dev "$IFACE" || true`. If `os.startup_scripts[].after` is present, it must be the string `"network-online.target"`, not an array.
## Node Requirements
`nextcloud-app`: features `headless`, `ssh`. Write `/etc/nextcloud/config.php.example`:
```
<?php
$CONFIG = array (
'dbtype' => 'pgsql',
'dbhost' => '10.76.0.20',
'dbname' => 'nextcloud',
'dbuser' => 'nextcloud',
'dbpassword' => 'nextcloud',
'redis' => array('host' => '10.76.0.30', 'port' => 6379),
'memcache.local' => '\OC\Memcache\Redis',
'datadirectory' => '/var/lib/nextcloud/data',
'overwrite.cli.url' => 'https://cloud.lab',
);
```
Create `/var/lib/nextcloud/{data,apps}` mode `0750 ops:ops`. Add a Python stdlib service on `0.0.0.0:80` exposing:
- `GET /status.php` -> `200 {"installed":true,"maintenance":false,"productname":"Nextcloud","version":"compat-1.0","versionstring":"compat-1.0"}`
- `GET /ocs/v2.php/cloud/capabilities?format=json` -> `200 {"ocs":{"meta":{"status":"ok","statuscode":200},"data":{"version":{"string":"compat-1.0"},"capabilities":{}}}}`
- `GET /metrics` -> `nextcloud_compat_up 1`
Register `nextcloud-compat.service`.
`nextcloud-db`: features `headless`, `ssh`, `postgresql`; packages `postgresql`, `postgresql-client`. Listen on `0.0.0.0:5432`, best-effort create role/database `nextcloud` password `nextcloud`, allow `10.76.0.0/24`. Expose `:9187/metrics` with `pg_compat_up 1`.
`nextcloud-redis`: features `headless`, `ssh`, `redis`; packages `redis-server`. Bind to localhost plus `10.76.0.30`. Expose `:9121/metrics` with `redis_compat_up 1`.
`collabora`: features `headless`, `ssh`. Write `/etc/coolwsd/coolwsd.xml.example` with `<allowed_hosts><host desc="lab">10\.76\.0\..*</host></allowed_hosts>` and `<ssl><enable type="bool">false</enable></ssl>`. Add a Python stdlib service on `0.0.0.0:9980` exposing `GET /hosting/discovery` -> `200 <?xml version="1.0" encoding="utf-8"?><wopi-discovery><net-zone name="external-http"/></wopi-discovery>` with `Content-Type: text/xml`, `GET /hosting/capabilities` -> `200 {"convert-to":{"available":false},"hasMobileSupport":true,"hasProxyPrefix":false}`, `GET /metrics` with `collabora_compat_up 1`. Register `collabora-compat.service`.
## Scenario
Emit exactly one group scenario named `nextcloud-cloud-stack-validation`. Put `custom_tests[].assertions[]` inside the scenario entry; leave `scenarios[].tests` empty. Every assertion needs `on_vm`. Use only `port_listening`, `command_output`, and `http_responds`; do not emit `vm_boots`, `network_reachable`, or `service_running`.
- `Stack ports listen`: `port_listening` for `nextcloud-app:80`, `nextcloud-db:5432`, `nextcloud-redis:6379`, `collabora:9980`.
- `Nextcloud status`: on `nextcloud-app`, `curl -fsS http://localhost/status.php | jq -e '.installed == true and .productname == "Nextcloud"' >/dev/null && echo nc-ok`.
- `Nextcloud capabilities`: on `nextcloud-app`, `curl -fsS 'http://localhost/ocs/v2.php/cloud/capabilities?format=json' | jq -e '.ocs.meta.statuscode == 200' >/dev/null && echo caps-ok`.
- `Collabora discovery`: on `collabora`, `curl -fsS http://localhost:9980/hosting/discovery | grep -q 'wopi-discovery' && echo collabora-ok`.
- `App reaches backends`: on `nextcloud-app`, `nc -z -w 5 10.76.0.20 5432 && nc -z -w 5 10.76.0.30 6379 && nc -z -w 5 10.76.0.40 9980 && echo backends-reachable`.
Preserve warnings that real Nextcloud PHP application install (or AIO image), PHP-FPM tuning, opcache and APCu, Redis cluster setup, Collabora Online CODE binary distribution, S3/object-storage external storage, real TLS termination, backup of `/var/lib/nextcloud/data`, app store integrations, and `10.76.0.0/24` aliasing are deployment-time concerns.Driving OpenFactory from an AI agent instead of the browser? The same flow is exposed through the OpenFactory MCP server — submit the prompt programmatically, get the build-plan preview back, and call create_build / start_vm on the resulting recipes. Single-image builds go straight through the openfactory CLI.
The prompt produces a buildable preparation lab — the right topology, the right ports listening, deployment-time config templates dropped in the right places, and tiny compatibility services that prove the wiring works. A few things still sit outside the recipe and need operator attention before this carries real load:
Should I run Nextcloud Memories or a separate Immich stack for photos? If Nextcloud is already up, enable Memories first — accounts and permissions carry over and it's one fewer service. But its recognition runs slower than Immich's dedicated ML container; photo-first households usually add the Immich photo vault alongside Nextcloud. The two coexisting is a normal, supported setup.
Postgres or MariaDB? Either is supported; the lab picks Postgres. The non-negotiable is not SQLite past a couple of users — concurrent writes will bite. Pick one real database and wire Redis for locking, and most performance complaints evaporate.
Do I really need Collabora on its own VM? For one or two editors you can co-locate it. The moment several people open documents at once, CODE's rendering competes with the web UI for CPU — the split keeps both responsive, and it's free to fold back together later if you over-provisioned.
The natural next service is a password manager. See the Vaultwarden password vault post, and the Paperless-ngx document lab for scanned-paper search that pairs neatly with Nextcloud's file store. For locking down the kernel under regulated data, read the runtime attestation post. The Enterprise & GxP page covers fleet deployments, and pricing shows what building these on managed infrastructure costs versus your own runner.
OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.