Production-oriented OpenCloud deployment for VPS hosts. One deploy.yaml describes your stack; apply.sh renders configs, manages secrets, and starts containers with Caddy (TLS) and upstream opencloud-compose (unchanged).
This replaces the official test installer (curl -L https://opencloud.eu/install | bash), which runs an insecure localhost binary and prints "This is a fragile test setup, not suitable for production!"
Requirements: Linux VPS, Docker Compose v2, DNS pointing at the server, ports 80/443 open.
git clone --recurse-submodules https://github.com/your-org/opencloud-easy-deploy.git
cd opencloud-easy-deploy
bash ensure-dependencies.sh # Docker, uv, submodules, Python deps
bash wizard.sh # interactive: writes deploy.yaml and deploysIf you already cloned without submodules:
git submodule update --init --recursiveOr manually:
cp deploy.yaml.example deploy.yaml
# edit deploy.yaml — set your domain and paths
bash ensure-dependencies.sh
bash apply.sh| File / script | Role |
|---|---|
deploy.yaml |
Operator-owned desired state (commit this) |
.opencloud-easy-deploy/secrets.yaml |
Generated passwords (gitignored) |
opencloud-compose/.env |
Generated Docker Compose env (gitignored) |
bash ensure-dependencies.sh |
Install Docker, uv, submodule, Python deps |
bash apply.sh |
Converge config and start/restart services |
bash wizard.sh |
Interactive deploy.yaml creator + apply |
bash start.sh / stop.sh |
Lifecycle without re-rendering |
bash update.sh |
Pull submodule + images, re-apply, restart |
bash backup-bundle.sh |
Portable .tar.gz for VPS migration |
bash restore-bundle.sh |
Restore from portable bundle on a fresh VPS |
bash restore-borg.sh |
Restore from Borg (local or SFTP) on a fresh VPS |
bash backup.sh |
Run Borg backup now (when backup.enabled) |
bash restore.sh |
List or restore from Borg archives |
bash uninstall.sh |
Remove generated runtime files (keeps data) |
See deploy.yaml.example. Key sections:
- opencloud — domain, image tag, persistent
data_dir/config_dir/apps_dir - proxy —
caddy(only option in v1) - auth —
builtin(simple admin login) oroidc(external IdP) - weboffice —
euro_officeorcollabora(mutually exclusive with each other) - modules — optional search, antivirus, radicale, monitoring
- backup — optional Borg backups to a local directory or SFTP repository
Uses OpenCloud's built-in LDAP. Admin password is generated on first apply.sh (or set via wizard) and stored in .opencloud-easy-deploy/secrets.yaml.
Set auth.mode: oidc and configure auth.oidc in deploy.yaml. The stack adds idm/external-idp.yml plus a local overlay for role mapping via proxy.yaml.
-
Create groups for OpenCloud roles:
opencloud-adminopencloud-useropencloud-guest
Match names to
auth.oidc.role_mappingindeploy.yaml. -
Create an OAuth2/OpenID provider application:
- Slug:
opencloud - Client type: Public
- Redirect URIs / Origins (strict):
https://<OC_DOMAIN>/https://<OC_DOMAIN>/oidc-callback.htmlhttps://<OC_DOMAIN>/oidc-silent-redirect.html
- Scopes:
email,offline_access,openid,profile - Bindings: attach the three groups above
- Slug:
-
Set deploy.yaml (example):
auth:
mode: oidc
oidc:
issuer_url: https://authentik.example.com/application/o/opencloud/
account_url: https://authentik.example.com/if/user/
domain: authentik.example.com
client_id: opencloud
client_scopes: "openid profile email offline_access"
role_claim: groups
role_mapping:
admin: opencloud-admin
user: opencloud-user
guest: opencloud-guest- Run
bash apply.sh.
Configure separate OAuth clients in your IdP:
| Client | Client ID | Redirect URI / Origin |
|---|---|---|
| Android | OpenCloudAndroid |
(strict) oc://android.opencloud.eu |
| iOS | OpenCloudIOS |
(strict) oc://ios.opencloud.eu |
| Desktop | OpenCloudDesktop |
(regex) http://127.0.0.1.*, http://localhost.* |
Internet → Caddy (:443, Let's Encrypt)
├── cloud.example.com → opencloud:9200
└── eurooffice.example.com → euro-office:80
opencloud-compose stack (docker network: opencloud-net)
├── opencloud
├── euro-office (optional)
├── ldap-server (OIDC mode only)
└── optional modules (tika, clamav, …)
Upstream compose files live in the opencloud-compose/ git submodule. Customizations are in overlays/ and rendered configs — never edit the submodule directly. Update with:
bash update.sh| Setting | Test install script | opencloud-easy-deploy |
|---|---|---|
| TLS | self-signed / insecure | Caddy + Let's Encrypt |
INSECURE |
true |
false |
DEMO_USERS |
enabled | false |
| Secrets | hardcoded | generated, mode 0600 |
| Config | ephemeral | persistent paths outside submodule |
Two ways to recover on a fresh VPS:
| Method | You provide | Everything else |
|---|---|---|
| Portable bundle | opencloud-backup-*.tar.gz |
bash restore-bundle.sh |
| Borg (local or SFTP) | Passphrase + how to reach the repo (SFTP host/user/path + SSH key, or local repo path) | deploy.yaml and data come from the archive |
On the running server (stop OpenCloud first for a clean snapshot):
bash stop.sh
bash backup-bundle.sh
# → opencloud-backup-cloud.example.com-20260729T120000Z.tar.gzCopy that single file to your new VPS (scp, rsync, object storage, etc.).
On a fresh VPS:
git clone <repo> opencloud-easy-deploy
cd opencloud-easy-deploy
bash restore-bundle.sh /path/to/opencloud-backup-*.tar.gzThat one command installs Docker, uv, restores all data/config/secrets, and runs apply.sh. Point DNS at the new server before visiting the URL.
The bundle contains passwords and keys — treat it like a secrets backup.
Optional output format: bash backup-bundle.sh -o /tmp/backup.tar.zst (requires zstd).
For incremental backups on the same server or off-site via SFTP, enable Borg in deploy.yaml:
Local repository (simple, same VPS):
backup:
enabled: true
repository:
type: local
path: /var/backups/opencloud
schedule:
enabled: true
calendar: "*-*-* 03:00:00"
persistent: true
retention:
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
keep_yearly: 0SFTP off-site (recommended for crash recovery):
backup:
enabled: true
repository:
type: sftp
host: backup.example.com
user: borg
path: /repos/opencloud
port: 22
ssh_key_path: /root/.ssh/borg_backup
host_key_check: true
schedule:
enabled: true
calendar: "*-*-* 03:00:00"
retention:
keep_daily: 7
keep_weekly: 4
keep_monthly: 6Each backup archive includes OpenCloud data, deploy.yaml, and secrets.yaml — restore pulls configuration from the archive, not a separate bootstrap file.
Then run bash apply.sh to generate backup config and a BORG_PASSPHRASE in .opencloud-easy-deploy/secrets.yaml. Store the passphrase and SSH private key safely off-site.
Run a backup now:
bash backup.shList archives:
bash restore.sh --listRestore on this host:
bash stop.sh
bash restore.sh --latest
bash apply.sh
bash start.shRestore on a fresh VPS from Borg:
git clone <repo> opencloud-easy-deploy && cd opencloud-easy-deploy
export BORG_PASSPHRASE='your-passphrase'
# SFTP:
export OCD_BACKUP_SFTP_HOST=backup.example.com
export OCD_BACKUP_SFTP_USER=borg
export OCD_BACKUP_SFTP_PATH=/repos/opencloud
export OCD_BACKUP_SSH_KEY=/root/.ssh/borg_backup
# Or local repo path:
# export OCD_BACKUP_LOCAL_PATH=/var/backups/opencloud
bash restore-borg.shrestore-borg.sh fetches deploy.yaml from the archive first, then restores all data using the paths defined there.
Backups include OpenCloud data/config/apps, LDAP state (if OIDC), Euro Office data, deploy.yaml, and secrets.yaml.
For scheduled backups, apply.sh writes systemd unit files to .opencloud-easy-deploy/backup/systemd/. Install them:
sudo cp .opencloud-easy-deploy/backup/systemd/opencloud-backup.* /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now opencloud-backup.timerStore the Borg passphrase and SSH key safely off-site — without them, backups cannot be restored. For local repos, sync /var/backups/opencloud elsewhere or use backup-bundle.sh periodically. For SFTP, the remote repository is your off-site copy.
Python dependencies are managed with uv. Install uv, then:
uv sync --dev
uv run pytest testsApply without starting containers (render only):
bash apply.sh --no-reconcile-runtimeIf OpenCloud logs show WopiDiscovery: wopi app url failed with unexpected code HttpCode=502, common causes are:
-
Caddy cannot reach backend containers — Caddy runs in a separate Compose project from OpenCloud/Euro Office. This project sets stable
container_namevalues (opencloud,euro-office) and generates a network overlay so Caddy can proxy to them onopencloud-net. -
Hairpin NAT / internal HTTPS calls — OpenCloud fetches
https://<euro-office-domain>/hosting/discoveryfrom inside Docker. The generated overlay maps your public domains tohost-gatewayso those requests reach Caddy on the host without relying on NAT loopback. -
JWT mismatch — Euro Office
JWT_SECRETmust match OpenCloudCOLLABORATION_WOPI_SECRET(notCOLLABORATION_JWT_SECRET, which breaks internal REVA tokens). Both are set from.opencloud-easy-deploy/secrets.yamlon apply. If JWT was wrong on first boot, remove<data-root>/euro-officeand re-apply so Euro Office regenerates its persisted secrets. -
X-Frame-Options / iframe blocked — If the browser console shows Euro Office blocked by
X-Frame-Options: sameorigin, re-runbash apply.shso Caddy setsContent-Security-Policy: frame-ancestorsfor the Euro Office domain instead.
Euro Office first boot can take 3–5 minutes (fonts, caches). apply.sh waits for WOPI discovery before restarting OpenCloud.
After updating, recreate the stack (a plain stop.sh / start.sh is not enough when networking overlays change):
bash apply.sh
bash diagnose.shcurl -fsS "https://eurooffice.example.com/hosting/discovery" | headSee upstream opencloud-compose for OpenCloud licensing. This deployment tooling is provided as-is.