# OpenCloud guide Everything beyond the first start: configuration, Files and S3, the web office, production and rolling images, the reverse proxy, branding, building, updating, troubleshooting and how the image is put together. ## Configuration | Variable | Default | Description | |---|---|---| | `IDM_ADMIN_PASSWORD` | *(required)* | Password for the built-in `admin` user, applied on first init. **Set this.** | | `OC_URL` | `https://192.168.1.10:9200` | **Required.** Public URL clients use to reach OpenCloud, and the OIDC login issuer. Must be `https`. Set your server's real LAN `IP:9200`, or your external hostname behind a reverse proxy. Unraid does not auto-fill this. | | `OC_INSECURE` | `true` | Accept the container's self-signed cert. Set `false` when a proxy provides a valid cert. | | `OC_LOG_LEVEL` | `info` | Log verbosity: `info`, `warn`, `error`, `debug`. | | `IDM_CREATE_DEMO_USERS` | `false` | Seed demo users (test only, unsafe for real use). | | `PROXY_TLS` | `true` | OpenCloud terminates TLS itself on 9200. Set `false` behind a TLS-terminating proxy (see [§6](#reverse-proxy)). | | `PROXY_ENABLE_APP_AUTH` | `false` | Let WebDAV clients sign in with a username and an **app token**. Needed by rclone and by phone sync apps, which cannot do the browser sign-in. Off by default; see [§10](#troubleshooting). | | `BRANDING_APP` | `false` | Add a **Branding** app where admins set the instance name, slogan, logos, favicon and login background. See [§7](#branding). | | `PUID` | `99` | User ID OpenCloud runs as, Unraid's *nobody*. | | `PGID` | `100` | Group ID, Unraid's *users*. | | Port | Purpose | | Volume | Purpose | |---|---|---|---|---| | `9200` | HTTPS WebUI / API (self-signed by default) | | `/etc/opencloud` | Config (`opencloud.yaml` + secrets) | | | | | `/var/lib/opencloud` | Data: user files, index, `nats` bus | | | | | `/files` | Files (optional): user files kept apart from Data, see [below](#files-in-a-separate-folder-optional) | > **No database.** OpenCloud is *not* Nextcloud. It has no MySQL/Postgres and needs none. State lives in the local storage tree on the `/var/lib/opencloud` volume plus an embedded NATS bus. Don't add a database container; there's nothing to point it at. > **Files show up but are greyed out / won't open?** The storage driver is wrong. Keep `STORAGE_USERS_DRIVER=posix` (the default): never leave it blank and never use `local`; both leave files visible but unreadable. Also make sure the Data volume is on a filesystem with extended-attribute support (the Unraid array and cache/pool disks have it). The driver is fixed at first init. To change it, start with a fresh Data folder. ### Where Config, Data and Files go OpenCloud has no database, so its file system is the database. Every file and folder carries its metadata as extended attributes, index files sit next to them, and the NATS message bus in Data saves each message to disk straight away. During a sync that means a steady stream of small writes. On Unraid, every `/mnt/user/...` path runs through the share layer (shfs), a single program that joins the cache and the array disks into one folder tree. Under that load it can stop answering. OpenCloud then waits for its files, while the CPU stays idle, memory stays free and no disk is busy. Every other container with a `/mnt/user` path hangs too, a restart from the Docker tab ends in an error, and only a reboot of the server helps. So none of OpenCloud's folders may go through the share layer: | Field | Where it goes | Example | |---|---|---| | **Config** | the SSD pool itself | `/mnt/cache/appdata/opencloud/config` | | **Data** | the SSD pool itself | `/mnt/cache/appdata/opencloud/data` | | **Files** (optional) | one array disk, if your files don't fit on the pool | `/mnt/disk1/opencloud/files` | - Use the pool and disk names from your **Main** tab. A path to a pool that doesn't exist ends up in RAM, see [the log ends with `unable to publish event`](#the-log-ends-with-unable-to-publish-event-and-fatal-error---exiting). - `/mnt/user/appdata/...` only skips the share layer when `appdata` is an exclusive share: it lives on one pool with **Secondary storage** set to **None**, and **Settings → Global Share Settings → Permit exclusive shares** is on. Giving the pool path is the safer choice. - For Files, set the share on the **Shares** tab to **Primary storage: Array**, **Secondary storage: None**, and include only the disk you give in the path. Then the mover leaves the folder alone, and nothing written to the share over the network lands on the cache or another disk, where OpenCloud wouldn't see it. - Writing straight to an array disk is slower than the SSD, but there's nothing in between that can hang. The container log warns at every start when Data or Files still goes through the share layer (`Data goes through Unraid's share layer`, `Files goes through Unraid's share layer`). To check the paths yourself, run this in the Unraid terminal: ```bash for p in $(docker inspect -f '{{range .Mounts}}{{.Source}} {{end}}' OpenCloud); do echo "$p"; df -hT "$p" | tail -n 1; done ``` None of the lines may start with `shfs`. A pool shows its device (for example `/dev/nvme0n1p1` or `/dev/sdf1`), an array disk `/dev/md1p1` for Disk 1.
### Files in a separate folder (optional) Data holds OpenCloud's message bus, search index and accounts. They write to disk all the time, and on the Unraid array that is slow enough to stall large syncs, so Data belongs on an SSD or cache pool. Your files can live elsewhere. If the pool is too small for them, set **Files** in the template to a single array disk, for example `/mnt/disk1/opencloud/files`. A `/mnt/user/...` share path goes through Unraid's share layer (shfs), which a large sync can hang along with every other container that uses it, so give the disk itself: on the **Shares** tab set the share's **Primary storage** to **Array**, **Secondary storage** to **None** and include only that disk. The files go there, and Data stays small: in a test with 4 GB in 2,000 files it held 41 MB. The wrapper notices the mount at `/files`, points OpenCloud's storage there (`STORAGE_USERS_POSIX_ROOT`) and hands the folder to `PUID`:`PGID`. Uploads are staged next to the files, so they still land on the array, but the message bus does not. With an S3 backend this field does nothing, because the files go to the bucket. Set Files before the first start. On an install that already has files, OpenCloud starts on the new, empty folder, the old files stay in Data without showing up, and the log says so on every start. To move them over: 1. Stop the container: **Docker** tab, click the OpenCloud icon, **Stop**. 2. Open the Unraid terminal (`>_` at the top right). Each account has a folder with the same name in both places: ```bash ls /mnt/cache/appdata/opencloud/data/storage/users/users/ /mnt/disk1/opencloud/files/users/ ``` Use your own Data and Files paths if they differ from these. 3. Copy the files over without OpenCloud's own `.oc-*` folders, and give them to the user OpenCloud runs as: ```bash rsync -r --exclude='.oc-*' "/mnt/cache/appdata/opencloud/data/storage/users/users//" "/mnt/disk1/opencloud/files/users//" chown -R nobody:users "/mnt/disk1/opencloud/files/users/" ``` With your own `PUID`/`PGID`, use those numbers instead of `nobody:users`. 4. Start the container and check that the files are there. Then delete `/mnt/cache/appdata/opencloud/data/storage/users/users` to free the space, and the warning goes away.
### S3 object storage (optional) OpenCloud can keep file **blobs** in any S3-compatible bucket while the metadata stays local. Set the storage driver to `decomposeds3` and add the connection variables. Keep the driver on `posix` (the default) for normal local storage. Do **not** leave it blank. For self-hosted, two genuine, actively-maintained S3-compatible stores work well here: **[SeaweedFS](https://github.com/junkerderprovinz/unraid-apps/tree/main/seaweedfs)** (recommended for a single node) and **[Garage](https://github.com/junkerderprovinz/garage)** (built for geo-distributed multi-node clusters, but runs single-node here too). AWS S3, Backblaze B2 and Wasabi work the same way against their own endpoints. | Variable | Example | Description | |---|---|---| | `STORAGE_USERS_DRIVER` | `decomposeds3` | Set to `decomposeds3` for S3 blob storage. Default is `posix` (local): never leave it blank or use `local`, both grey out files. | | `STORAGE_USERS_DECOMPOSEDS3_ENDPOINT` | `http://192.168.1.10:8333` | S3 endpoint. Internal `http://` URL for self-hosted SeaweedFS/Garage; the provider's `https://` endpoint for AWS/B2/Wasabi. | | `STORAGE_USERS_DECOMPOSEDS3_REGION` | `default` | `default` for SeaweedFS, `garage` for Garage (its default region), or the provider region (`us-east-1`, …) otherwise. | | `STORAGE_USERS_DECOMPOSEDS3_ACCESS_KEY` | `…` | Access key ID. | | `STORAGE_USERS_DECOMPOSEDS3_SECRET_KEY` | `…` | Secret access key. | | `STORAGE_USERS_DECOMPOSEDS3_BUCKET` | `opencloud` | Bucket name: **create it first**, the container does not. | **Where those values come from:** - **SeaweedFS (self-hosted).** In that template, set an **Access Key** and **Secret Key** (see its README's Security note) and optionally a **Pre-create Bucket** name. Those become your access key, secret key and bucket directly, no separate service-account step. Point the endpoint at its S3 port, `http://:8333`, with region `default`. - **Garage (self-hosted).** In that template, set an **Access Key** and **Secret Key** (and optionally a **Bucket**). They are pre-seeded on first boot, no separate CLI step. Point the endpoint at its S3 API port, `http://:3900`, with region `garage`. - **AWS S3 / Backblaze B2 / Wasabi.** Create a bucket in the provider console, then create an access key (AWS: an IAM access key; B2/Wasabi: an application/API key). Use the provider's `https://` endpoint and the bucket's region. > **The metadata always stays local.** `decomposeds3` puts only the blob bytes in S3; the file tree, xattrs and the blob→object mapping live on `/var/lib/opencloud`. That volume is therefore **required and must be backed up even with S3**. Losing it orphans your S3 objects (they are opaque IDs with no folder structure). There is no all-on-S3 mode. OpenCloud's system/metadata store (`STORAGE_SYSTEM_DRIVER`) stays `decomposed` (local) and needs no change. If uploads fail with a checksum error on a non-AWS endpoint, add `STORAGE_USERS_DECOMPOSEDS3_PUT_OBJECT_DISABLE_CONTENT_SHA256=true`. Both the SeaweedFS and Garage paths use the same generic `decomposeds3` driver this wrapper's S3 support was originally built and verified against. SeaweedFS has since been re-verified live against a real OpenCloud instance after the switch away from MinIO (connectivity, boot health and unauthenticated bucket reachability all confirmed; a fully authenticated file-read round-trip is the one check still outstanding). Garage has not yet been separately re-verified end-to-end.
### Web office (optional) OpenCloud can edit documents in the browser, but it ships no office engine: it speaks the **WOPI** protocol to a **separate document-server container**. This wrapper wires that up from three template fields (default off). **With Euro Office** (recommended): 1. Install the [Euro Office](https://github.com/junkerderprovinz/euro-office) template (search **Euro Office** in Community Applications) and set its **JWT secret**. 2. In this template set **Web office suite** = `euro-office`, **Office document server URL** = the address its WebUI button opens, e.g. `http://192.168.1.10:9900`, and **Office WOPI secret** = the same text as that JWT secret. 3. Apply. The **New** button now offers documents, spreadsheets and presentations, and existing Office files open in the editor. OpenCloud then serves the editor under its own address at `/euro-office/`, so the browser never talks to Euro Office directly. Plain http works, there is no second certificate to accept, and from outside (reverse proxy, Tailscale) only OpenCloud has to be reachable. An `https://` address such as `https://192.168.1.10:9943` works too. **With Collabora Online (CODE)** (`collabora`, image `collabora/code`, port 9980, community CA template) or **OnlyOffice Document Server** (`onlyoffice`, image `onlyoffice/documentserver`, `WOPI_ENABLED=true`): the browser loads the editor straight from that server, so **Office document server URL** has to be its **https** address. An `http://` address leaves the editor blank, and the startup log says so. For Collabora, set its WOPI host allowlist (`aliasgroup1` / `domain`) to your OpenCloud URL; it needs no WOPI secret. For OnlyOffice the secret must equal its JWT secret. Under the hood the wrapper turns on OpenCloud's built-in `collaboration` service, sets the `COLLABORATION_*` variables, registers it as the secure-view/edit handler and exposes the secure-view role. It writes a `csp.yaml` and points `PROXY_CSP_CONFIG_FILE_LOCATION` at it. For Euro Office it also adds the `/euro-office/` and `/sdkjs/` routes to the managed `proxy.yaml`, and the `csp.yaml` allows `'unsafe-eval'` scripts, `blob:` workers and `data:` fonts, which the editor needs and OpenCloud's own policy forbids. That loosens OpenCloud's Content-Security-Policy only while **Web office suite** is `euro-office`. If you keep a `proxy.yaml` of your own, the wrapper leaves it alone and Euro Office is reached directly, so its address then has to be https; if you set `PROXY_CSP_CONFIG_FILE_LOCATION` yourself, the startup log prints the directives to add. ### Full-text search (Apache Tika, optional) OpenCloud already has a built-in search: out of the box it matches **file and folder names** and metadata (tags, media type, …). It does not look **inside file contents** on its own. **Apache Tika** is not a second search engine, it is a text-extractor that OpenCloud's search service uses to read the text out of documents (PDF, Word, Excel, PowerPoint, ODF, …) so a search word inside a file is found too. (The `TIKA=:tika.yml` / `TIKA_IMAGE` lines you may have seen belong to OpenCloud's official *docker-compose* deployment. This Unraid wrapper has no `.env`; the two template fields below do the wiring instead.) 1. **Run Apache Tika** (its own container). Ready-made **Tika templates exist in Community Applications** (search *Tika*). Install one (image `apache/tika`, port `9998`; a `-full` tag additionally does OCR of scanned images). Note its network-reachable address, e.g. `http://:9998`. 2. **Turn it on:** set **Full-text search (Tika)** to `true` and **Tika server URL** to that address, then **Apply**. The wrapper points OpenCloud's search extractor at Tika and switches full-text search on for you. Under the hood the wrapper sets `SEARCH_EXTRACTOR_TYPE=tika`, `SEARCH_EXTRACTOR_TIKA_TIKA_URL` and `FRONTEND_FULL_TEXT_SEARCH_ENABLED=true` (plus `SEARCH_EXTRACTOR_CS3SOURCE_INSECURE=true` for the internal LAN cert). Only files uploaded or changed **after** this are content-indexed; existing files are **not** re-indexed automatically, so re-upload or edit a file to test. See the [OpenCloud search docs](https://docs.opencloud.eu/docs/dev/server/Services/search/Search-info/).
## Production vs Rolling Two channels are built from this wrapper, differing only in the upstream base image: | Tag | Base image | For | |---|---|---| | `junkerderprovinz/opencloud:rolling` | `opencloudeu/opencloud-rolling:latest` | **Default.** Newest OpenCloud releases (currently 8.x), published about every three weeks. | | `junkerderprovinz/opencloud:production` | `opencloudeu/opencloud:latest` | OpenCloud's production line (currently the 7.2.x train), fully QA'd and cut about every six months. `:latest` is the same line at this image's newest release, so it moves only with a release. | **Which channel?** Rolling is the default because the production line still carries two problems that bite on Unraid. As of 7.2.x it lacks the incremental-fsync fix (reva#720) for the large-folder sync abort on slow storage (issue #3027), which shipped in 7.3.0. More seriously, it treats a failed postprocessing event publish as fatal and ends the whole server process, so a single transient `nats: timeout` can take the container down; that was fixed in 7.5.0 ([#3347](https://github.com/opencloud-eu/opencloud/pull/3347)), which retries the publish. A bus that stays silent for longer still stops the server on either channel, see [Troubleshooting](#the-log-ends-with-unable-to-publish-event-and-fatal-error---exiting). Since production is cut roughly twice a year, the stable line will not carry the fix for months. Pick `:production` instead if you would rather have OpenCloud's fully QA'd line and your data volume already sits on a fast SSD/NVMe pool, which avoids the stall on its own. Switch by changing the **Repository** tag in the Unraid template. Back up your appdata before switching channels. Both channels track OpenCloud's own upstream `:latest` tag directly, and the weekly rebuild picks it up automatically alongside Alpine security patches, with no waiting on a version-bump PR to get merged.
## How the Wrapper Works The entrypoint runs as root only long enough to prepare the volumes, then drops to your user: 1. **Permission heal.** Creates `/etc/opencloud` + `/var/lib/opencloud` if missing and `chown`s them to `PUID:PGID`. The config dir is small and always fully healed; the data dir is only `chown -R`'d once (or after a `PUID`/`PGID` change), tracked by a `.uid-heal` sentinel, so a large data set is never recursively re-owned on every boot. The `nats` bus dir is always re-asserted (small, must stay writable). 2. **Branding app.** With `BRANDING_APP=true` the entrypoint copies the web extension into the data volume, writes the managed `proxy.yaml` (unless you have your own, see [§7](#branding)) and starts `brandingd` as `PUID:PGID` on `127.0.0.1:9299`. It also points `IDP_ASSET_PATH` at the image's copy of OpenCloud's login page, unless you set that variable yourself. The copy adds one script, which loads the saved branding from `brandingd`. With `false` it removes the extension and the managed `proxy.yaml`. Whenever a saved branding exists, it also runs `brandingd -regenerate` once, app on or off, so the branding follows the base theme of the current image. 3. **Search index check.** A bleve search index that OpenCloud cannot open would make the `search` service fail five times, and then the whole server stops. `searchindex` reads each index the way bleve loads it, and moves one that would fail to `.broken`, so the service starts with a new, empty index. When a new index appears on data that already had one (after that move, or when an image update changes the index schema), the entrypoint indexes all spaces again in the background once the server is up. See [§10](#troubleshooting). 4. **Init.** Runs `opencloud init` as the target user (writes `opencloud.yaml`, consuming `IDM_ADMIN_PASSWORD`). It is idempotent and harmlessly errors once the config exists. 5. **Hand-off.** Prints the ready banner, then `exec`s `opencloud server` dropped to `PUID:PGID` via a static `gosu` (copied from the upstream `tianon/gosu` image, so the base needs no package manager).
## Reverse Proxy By default OpenCloud serves HTTPS itself on `9200` with a self-signed certificate, ideal for a direct LAN install. To put it behind a reverse proxy that terminates TLS (Traefik, NGINX Proxy Manager, SWAG, …): - set **`PROXY_TLS=false`** (OpenCloud then serves plain HTTP for the proxy to wrap), - set **`OC_URL`** to your external URL, e.g. `https://cloud.example.com`, - set **`OC_INSECURE=false`** (your proxy presents a valid certificate), - point the proxy upstream at the container's port `9200`. **Desktop or mobile client login returns `403 Forbidden` while the browser works?** The native clients sign in through a loopback OIDC redirect (`redirect_uri=http://127.0.0.1:`, per RFC 8252). Many reverse-proxy "block exploits" filters reject a literal `http://` inside a query string. In **NGINX Proxy Manager** this is the **"Block Common Exploits"** toggle: its `block-exploits.conf` contains `if ($query_string ~ "[a-zA-Z0-9_]=http://") { return 403; }`. The web UI avoids it (its redirect is URL-encoded), the desktop client trips it (plain `http://` loopback). Switch that toggle **off** for the OpenCloud host and the client login succeeds. OpenCloud brings its own auth and CSRF protection, so the crude regex filter is redundant here.
## Branding Set **Branding admin app** (`BRANDING_APP`, in the advanced view of the template) to `true` and restart the container. Accounts with the Admin role then find **Branding** in the app menu. Other accounts do not get the entry, and the service behind it refuses their changes. In the app an admin can set: - the instance name and slogan - a logo, plus an optional one for dark mode (without it, dark mode uses the logo) - the favicon - the background of the login page - whether the sign-in card is light, dark, or follows the browser In the web UI, the name appears in the browser tab and the slogan on public link pages and the sign-out page. On the login page, the name goes into the tab title, and name and slogan replace OpenCloud's in the footer. With a name and no slogan, the footer shows only the name. The login page also shows your logo and favicon. OpenCloud's sign-in card is white. Under **Login page** you can turn it dark in the colours of the web UI's dark theme, or let it follow the visitor's browser setting. The background behind the card stays as it is either way. Images can be PNG, JPEG, GIF, WebP or SVG, up to 5 MB for each logo, 2 MB for the favicon and 25 MB for the background. SVG files are rebuilt on upload from shapes, paths, text, groups, symbols, gradients, masks, clip paths and embedded images, with their styling in attributes or `style=`. The rebuild drops `