
A four-VM cloud productivity stack: Nextcloud + Postgres + Redis + Collabora, from one prompt
May 23, 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 in the top of every r/selfhosted survey and the seventh-most-deployed in 2026.
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.
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.Four Debian Trixie VMs on 10.76.0.0/24. The app server is the only one with public-facing intent; the database, cache, and office host stay on the lab subnet.
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:
Cloud storage gets a password manager next. See the Vaultwarden password vault post. For locking down the kernel under regulated data, read the runtime attestation post. And the Enterprise & GxP page covers fleet deployments.
OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.