A plug-and-play Docker image that turns the official OpenCloud server into a genuine
one-click Unraid app: it runs the required first-boot init for you, heals the
appdata permissions and honours Unraid's PUID/PGID — no console,
no chown, no config-file editing required.
A one-knight job: I build it, keep it running, work through the issues and add what people ask for, until nothing is missing. It is free, with no accounts, no telemetry, no ads and no paid tier. No asterisk anywhere. Nothing readable ever leaves your own walls. Forged on evenings and weekends, with heart and stubbornness.
If it has earned a place on your server or computer, toss a coin to your knight: it helps cover the costs and keeps the project alive. It also makes this knight's heart beat a little faster. Three ways below, whichever suits you.
- Overview
- Quick Start
- Configuration
- Production vs Rolling
- How the Wrapper Works
- Reverse Proxy
- Branding
- Building Locally
- Updating
- Troubleshooting
- Architecture
- Contributing / License
- License
- How AI is used here
- Support this project
OpenCloud is a modern, self-hosted file sync-and-share platform (an actively developed member of the ownCloud/Infinite-Scale family). The official opencloudeu/opencloud image is excellent, but it is not built for a one-click NAS install:
- it runs its binary as a fixed UID with no
PUID/PGIDsupport, so on a fresh Unraid box the root-owned bind mounts make the very first boot fail with "permission denied" writing/etc/opencloud/opencloud.yamland/var/lib/opencloud/nats; - it requires a one-time
opencloud initto be run by hand beforeopencloud serverwill start.
This image is a thin wrapper around the official one that fixes those two things and adds a few extras:
- Auto-init — runs
opencloud initonce on first boot (idempotent on later boots). - Permission heal — creates the config/data dirs and hands them to your
PUID:PGID, and repairs a previously root-owned tree once (sentinel-guarded, so it never recursively re-chowns your whole data set on every start). - PUID / PGID — drops privileges to Unraid's
nobody:users(99:100) by default via a staticgosu. - Two channels —
:rolling(newest builds, the template default) and:latest(OpenCloud's fully QA'd production line), from the same wrapper. - Multi-arch — amd64 and arm64.
- Branding app (optional, off by default): set the name, logos, favicon and login background from the web UI, see §7.
The wrapper does not fork, patch or repackage OpenCloud itself. It layers a tiny entrypoint and the optional branding app on top of the unmodified upstream image, so you always run real, current OpenCloud.
The OpenCloud web UI: your personal space with files, folders and spaces.
Sign in as admin with the password you set in the Unraid template.
On Unraid: Apps → search for OpenCloud → Install. The Community Applications template is published from the unraid-apps feed.
To load it by hand:
mkdir -p /boot/config/plugins/dockerMan/templates-user && \
curl -fsSL -o /boot/config/plugins/dockerMan/templates-user/my-OpenCloud.xml \
https://raw.githubusercontent.com/junkerderprovinz/unraid-apps/main/opencloud/opencloud.xmlIn the template, the only field you must set is Admin Password (IDM_ADMIN_PASSWORD) — it becomes the password for the built-in admin user on first start. The two volumes default to /mnt/user/appdata/opencloud/{config,data}; adjust the data path to a share with room to grow.
Hit Apply. The first start takes a moment while the container generates its config and a self-signed certificate. Watch the container log for:
OpenCloud
OPENCLOUD IS READY
Open https://<unraid-ip>:9200/ and accept the self-signed certificate once. Log in as admin with the password you set.
Plain Docker (no Unraid)
docker run -d \
--name opencloud \
--restart unless-stopped \
-p 9200:9200 \
-e PUID=99 -e PGID=100 \
-e IDM_ADMIN_PASSWORD='change-me-please' \
-e OC_URL='https://192.168.1.10:9200' \
-e OC_INSECURE=true \
-v /mnt/user/appdata/opencloud/config:/etc/opencloud \
-v /mnt/user/appdata/opencloud/data:/var/lib/opencloud \
junkerderprovinz/opencloud:rollingSet OC_URL to how clients reach the server (its IP:port, or your proxied hostname).
| 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). |
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. |
BRANDING_APP |
false |
Add a Branding app where admins set the instance name, slogan, logos, favicon and login background. See §7. |
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 |
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/opencloudvolume 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 uselocal; 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.
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 (recommended for a single node) and 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://<seaweedfs-ip>:8333, with regiondefault. - 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://<garage-ip>:3900, with regiongarage. - 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.
decomposeds3puts 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) staysdecomposed(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.
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 (all advanced, default off).
- Run a document server (its own container):
- Euro Office (
euro-office) — the sovereign OnlyOffice fork, imageghcr.io/euro-office/documentserver, port 80. SetWOPI_ENABLED=true. There is a one-click Unraid template for it in the junkerderprovinz feed (search Euro Office in Community Applications). OpenCloud itself makes Euro Office the default editor for MS formats (docx/xlsx/pptx). - Collabora Online (CODE) (
collabora) — imagecollabora/code, port 9980. A maintained community CA template exists (search Collabora in Community Applications); set its WOPI host allowlist (aliasgroup1/domain) to your OpenCloud URL. Best for ODF (odt/ods/odp). - OnlyOffice Document Server (
onlyoffice) — imageonlyoffice/documentserver, port 80; setWOPI_ENABLED=true. A community CA template exists.
- Euro Office (
- Point OpenCloud at it: set Web office suite to
euro-office,collaboraoronlyoffice, Office document server URL to the server's browser-reachable URL, and an Office WOPI secret. For OnlyOffice and Euro Office that secret must equal the document server's JWT secret (JWT_SECRET/EURO_OFFICE_JWT_SECRET); for Collabora it is not required. - Reverse proxy: forward
/wopiand/collaborationto OpenCloud on port 9200, and make sure OpenCloud and the document server can reach each other over the network.
Under the hood the wrapper turns on OpenCloud's built-in collaboration service (OC_ADD_RUN_SERVICES=collaboration), sets the COLLABORATION_* variables, registers it as the secure-view/edit handler and exposes the secure-view role. It also writes a small csp.yaml adding the document server's origin to OpenCloud's Content-Security-Policy frame-src/img-src and points PROXY_CSP_CONFIG_FILE_LOCATION at it, so the editor iframe isn't CSP-blocked by the browser — a step OpenCloud's own reference deployment requires wiring by hand. Already set PROXY_CSP_CONFIG_FILE_LOCATION yourself? The wrapper leaves it alone; add the document server's origin to your own file's frame-src/img-src. Leave Web office suite on off (the default) if you do not need document editing.
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.)
- Run Apache Tika (its own container). Ready-made Tika templates exist in Community Applications (search Tika) — install one (image
apache/tika, port9998; a-fulltag additionally does OCR of scanned images). Note its network-reachable address, e.g.http://<TIKA_IP>:9998. - Turn it on: set Full-text search (Tika) to
trueand 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.
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:latest |
opencloudeu/opencloud:latest |
OpenCloud's production line (currently the 7.2.x train), fully QA'd and cut about every six months. :production is kept as an alias, same image. |
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). Slow storage is precisely what produces those timeouts. Since production is cut roughly twice a year, the stable line will not carry the fix for months.
Pick :latest 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 — no waiting on a version-bump PR to get merged.
The entrypoint runs as root only long enough to prepare the volumes, then drops to your user:
- Permission heal. Creates
/etc/opencloud+/var/lib/opencloudif missing andchowns them toPUID:PGID. The config dir is small and always fully healed; the data dir is onlychown -R'd once (or after aPUID/PGIDchange), tracked by a.uid-healsentinel — so a large data set is never recursively re-owned on every boot. Thenatsbus dir is always re-asserted (small, must stay writable). - Branding app. With
BRANDING_APP=truethe entrypoint copies the web extension into the data volume, writes the managedproxy.yaml(unless you have your own, see §7) and startsbrandingdasPUID:PGIDon127.0.0.1:9299. It also pointsIDP_ASSET_PATHat 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 frombrandingd. Withfalseit removes the extension and the managedproxy.yaml. Whenever a saved branding exists, it also runsbrandingd -regenerateonce, app on or off, so the branding follows the base theme of the current image. - Init. Runs
opencloud initas the target user (writesopencloud.yaml, consumingIDM_ADMIN_PASSWORD). It is idempotent and harmlessly errors once the config exists. - Hand-off. Prints the ready banner, then
execsopencloud serverdropped toPUID:PGIDvia a staticgosu(copied from the upstreamtianon/gosuimage, so the base needs no package manager).
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_URLto 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:<port>, 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.
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 <style> blocks, filters, patterns, markers and anything that could run code, so export logos with presentation attributes rather than CSS classes, or their colours are lost. An SVG has to be UTF-8 without DOCTYPE entities. One that would freeze the browser, such as masks nested in masks or references that loop back on themselves, is refused.
To upload an image, click its preview in the app. The menu next to its heading also resets it to the OpenCloud default. Saved changes need no restart: the app updates the page you have open, and every other page, the login page included, picks them up on its next load. Only switching BRANDING_APP on or off needs a container restart.
If you set IDP_ASSET_PATH yourself, the wrapper leaves it alone, and the app does not change the login page's title, footer, favicon or card. The page still shows your logo and background, but it learns only at container start whether there is a background, so adding the first one or removing it again needs a restart. The wrapper looks only at the environment variable: while the app is on, its own IDP_ASSET_PATH wins over an asset path in /etc/opencloud/idp.yaml, so set yours through the variable.
Switching BRANDING_APP back to false removes the app but keeps your branding. The login page then keeps your logo and background, and its title, footer, favicon and white card go back to OpenCloud's. To go back to the OpenCloud defaults, reset the fields in the app first.
Before you go back to an image without the app, set BRANDING_APP=false and start the container once so it removes the app and the managed proxy.yaml. An older image leaves both behind: Branding stays in the app menu, and its page cannot load. Your name, slogan, logos and favicon carry over to the web UI, but the login page keeps only the logo. If you already switched, delete /var/lib/opencloud/web/assets/apps/branding by hand, and /etc/opencloud/proxy.yaml too if it starts with # managed by the opencloud Unraid wrapper.
The app owns the name, slogan, logo, favicon and the whole clients.web.themes list in /var/lib/opencloud/web/assets/themes/_branding/theme.json. Other keys in that file, such as common.urls, stay as they are. From the first start with the app on, the wrapper rewrites the app's keys from its settings at every start, even with BRANDING_APP=false. Hand edits to those keys are replaced, and so is anything OpenCloud's own /branding/logo endpoint writes there. A hand-made themes list with custom colours does not survive either. On that first start, a theme.json that already sets any of these keys is copied to /var/lib/opencloud/branding/theme.json.before-branding-<time>, and the app takes over its name and slogan.
The app talks to its service through the proxy route /brandingsvc/. While the app is on, the wrapper writes that route to /etc/opencloud/proxy.yaml at every start, and it deletes the file again when you set BRANDING_APP=false. To add routes of your own to that file, delete its first line (the marker comment). The file is then yours, and the wrapper leaves it alone.
With your own proxy.yaml, the app turns on only if the file carries the route. Add it as one more item under routes: of the - name: default entry in your additional_policies, indented like the items already there:
- endpoint: /brandingsvc/
backend: http://127.0.0.1:9299
unprotected: trueIf the file has no additional_policies key yet, add the whole block below instead. Do not add a second additional_policies key: OpenCloud then ignores the whole file, your own routes included.
additional_policies:
- name: default
routes:
- endpoint: /brandingsvc/
backend: http://127.0.0.1:9299
unprotected: trueThe proxy only uses the route from the default policy; anywhere else the app could not load. The route also needs unprotected: true, because the login page loads the branding through it before anyone has signed in. When either is missing, the app stays off and the log says why. unprotected only skips the proxy's sign-in check, and the service still asks OpenCloud about every change.
The service finds OpenCloud's port through PROXY_HTTP_ADDR. To move OpenCloud to another port, set it there, not with http.addr in a proxy.yaml.
git clone https://github.com/junkerderprovinz/opencloud.git
cd opencloud
# production/latest channel (default base)
docker build -t opencloud:dev .
# rolling channel (reads the BASE_ROLLING pin from the Dockerfile)
docker build --build-arg BASE="$(grep -oE 'ARG BASE_ROLLING=[^[:space:]]+' Dockerfile | cut -d= -f2)" -t opencloud:rolling .
# multi-arch (amd64 + arm64) — needs buildx
docker buildx build --platform linux/amd64,linux/arm64 -t opencloud:dev --load .just build and just build-rolling build the two channels like CI does, and just lint runs the checks of the Lint workflow on the Dockerfile, the scripts, brandingd (Go tests without -race) and the web extension. just smoke runs only the base boot gate, not the branding smoke from CI.
docker pull junkerderprovinz/opencloud:latest
docker stop opencloud && docker rm opencloud
# re-create with the same template / docker run argsOn Unraid: Docker tab → the container → Force Update. Your /etc/opencloud and /var/lib/opencloud are untouched. The image is rebuilt weekly for upstream OpenCloud and Alpine patches.
The container starts, heals ownership, then crash-loops and the WebUI never comes up. This means the Data volume is pointing at a non-fresh OpenCloud/oCIS data directory — an old install, or a data set created with a different storage backend (local vs S3). The layouts are not interchangeable and there is no in-place migration between backends, so the search service can't open its index and takes the whole server down.
Fix: give it a fresh, empty Data folder. Move the old directory aside (mv /mnt/user/opencloud /mnt/user/opencloud.old) and let a new empty one be created, then restart. To keep old files, start fresh and re-upload them through the web UI. This is not a bug in the wrapper or the image — a clean data dir boots normally, S3 included.
rclone, a phone sync app or any other WebDAV client is refused with 401 Unauthorized, while the same account signs in through the browser without trouble. The token is not the problem: PROXY_ENABLE_APP_AUTH is false by default, and with it off the proxy refuses the request before the token is read at all.
Two details give it away, and both are visible in the container log. The 401 comes back in well under a millisecond, far too fast for anything to have been checked, and no line from auth-app or auth-basic appears anywhere near it: the services are running, they are simply never asked.
Fix: set PROXY_ENABLE_APP_AUTH=true and restart the container. Then create the token under Account → App tokens in the web interface, and give the client the account's user name with that token as the password. The token is a handful of words separated by spaces, so paste it whole rather than retyping it.
This does not open WebDAV to account passwords. That is the separate PROXY_ENABLE_BASIC_AUTH, which stays off, and an app token can be revoked on its own without touching the password.
Syncing a large folder (tens of GB) from the desktop client stalls partway and the client connection just drops. The cause is slow fsync on the Data volume, not the network. The Data volume holds the embedded NATS message bus, the file-tree metadata and the transient upload staging, all of them fsync-heavy. On slow storage (the Unraid array, or any /mnt/user share through the shfs FUSE union) the fsync storm freezes, postprocessing fails and the server drops the client (upstream issue #3027).
Fix: put the Data volume on a fast SSD/NVMe pool, not the array. With a decomposeds3/S3 backend only this small metadata volume needs fast storage (the file blobs go to your S3 bucket, so it stays small and grows with file count, not size). The reva incremental-fsync change (reva#720) also helps and ships from OpenCloud 7.3.0, which is on the :rolling channel (§4).
First start seems stuck / WebUI not reachable yet
The first boot runs opencloud init and generates a self-signed certificate — give it a moment. Watch the log for the OPENCLOUD IS READY banner, then open https://<ip>:9200/.
Browser warns about the certificate
That is expected with the default self-signed certificate (OC_INSECURE=true). Accept it once, or put OpenCloud behind a reverse proxy with a real certificate (see §6).
"permission denied" in the log
The wrapper heals ownership on start, but a data set created earlier as a different user can need a one-time repair. Stop the container, delete /var/lib/opencloud/.uid-heal, and start again to force a full re-chown to your PUID:PGID.
I forgot / want to change the admin password
IDM_ADMIN_PASSWORD is read on every start and overrides the stored admin password, so just set it in the template and restart.
Login loops or "redirect URI" errors behind a proxy
OC_URL must exactly match the URL in your browser (scheme + host + port). Set OC_URL to your external https URL and PROXY_TLS=false (see §6).
The saved branding is gone
If /var/lib/opencloud/branding/state.json is not valid JSON, the container moves it aside as state.json.invalid-<time>, names it in the log and falls back to the OpenCloud defaults. Your images stay. Fix the file, rename it back to state.json and restart. Do that before you save anything in the app, because a save deletes every image the new settings do not use.
┌──────────────────────────────────────────────────────────────┐
│ opencloudeu/opencloud[:latest] | opencloud-rolling │
│ (Alpine base + the OpenCloud binary, unmodified) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ entrypoint.sh (runs as root) │ │
│ │ ↓ mkdir + chown /etc/opencloud, /var/lib/opencloud │ │
│ │ ↓ one-time data heal (sentinel-guarded) │ │
│ │ ↓ BRANDING_APP: extension + proxy route │ │
│ │ ↓ brandingd -regenerate (if a branding is saved) │ │
│ │ ↓ BRANDING_APP: IDP_ASSET_PATH (login page copy) │ │
│ │ ↓ BRANDING_APP: brandingd on 127.0.0.1:9299 & │ │
│ │ ↓ gosu PUID:PGID opencloud init (|| true) │ │
│ │ ↓ print "OPENCLOUD IS READY" banner │ │
│ │ ↓ exec gosu PUID:PGID opencloud server │ │
│ └────────────────────────────────────────────────────────┘ │
│ multi-stage: static gosu ← tianon/gosu │
│ brandingd ← Go build stage │
│ web extension ← Node build stage │
│ base theme ← download from GitHub │
│ login page ← the OpenCloud binary │
└──────────────────────────────────────────────────────────────┘
Pull requests welcome. Issues: https://github.com/junkerderprovinz/opencloud/issues.
Licensing — dual:
- This wrapper repository (Dockerfile,
entrypoint.sh,print-banner.sh,branding/with brandingd and the web extension, Unraid template, README and banner/icon artwork) is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). - OpenCloud itself and the bundled
gosubinary are Apache-2.0; the Alpine base and its packages keep their own licenses. When you run, redistribute or rebuild the resulting image you must comply with all of those, not only this wrapper's AGPL-3.0 license. SeeNOTICE.
The OpenCloud logo and wordmark are the property of OpenCloud GmbH, used unmodified to identify the upstream project. This is an independent, community-maintained packaging and is not affiliated with or endorsed by OpenCloud GmbH.
- OpenCloud — the file sync-and-share platform this image wraps
- gosu — clean, static privilege-drop for the entrypoint
Copyright (C) 2026 Junker der Provinz.
This repository packages OpenCloud as a container for Unraid. The packaging in this repository (Dockerfile, scripts, theme, web assets and everything else original here) is free software under the GNU Affero General Public License v3.0 (AGPL-3.0); see LICENSE. If you distribute it, or run a modified version as a network service, you must release your source under the same AGPL-3.0 terms and keep the existing copyright and attribution notices intact.
Scope. The AGPL applies to this repository's own code and assets. OpenCloud itself is a separate project under its own license and name; this repository does not claim it. The banner, logo, theme and other branding original to this repository remain reserved: a fork must use its own branding and may not present itself as this project.
One knight builds this, and AI is one of the tools I work with, the same way I work with an editor or a compiler. It helps me write code and documentation and it checks my work, and that saves me a good many evenings. It does not make the decisions, though. I read and understand everything before it ships, and if something here breaks, that is on me and not on the tool.
You do not have to take my word for it. The code is open and every release note is written by hand. The issue tracker shows how problems actually get handled, including the ones I got wrong the first time. If you find something that is not right, open an issue and I will look at it.
Questions? Check the support thread. Bugs, ideas or feature requests? Please open a GitHub issue.
A one-knight job: I build it, keep it running, work through the issues and add what people ask for, until nothing is missing. It is free, with no accounts, no telemetry, no ads and no paid tier. No asterisk anywhere. Nothing readable ever leaves your own walls. Forged on evenings and weekends, with heart and stubbornness.
If it has earned a place on your server or computer, toss a coin to your knight: it helps cover the costs and keeps the project alive. It also makes this knight's heart beat a little faster. Three ways below, whichever suits you.