Import complete: 25 044 objs / 26 GB. 99.97% залито rclone'ом (22.2 GB за ~2.5 ч), оставшиеся 6 файлов (4.04 GB) — 4× .pat ~221 MB + 2× .seospider 1.55+1.7 GB — упали с 502/500 на каждом rclone PUT из-за 60-секундного HTTP timeout в reva v2.27 datagateway (hardcoded в Go http.Client.Timeout, не env-configurable). Workaround: VDS-side curl PUT loopback через throwaway sftp key. Local network 142 MB/s → 4.04 GB за 28 секунд, все PROPFIND size match. Wiki: §Gotcha 5 в concepts/ocis-on-vds-deploy-recipe.md документирует finding + recipe для workaround. Traefik buffering middleware пробован, не помог (likely vulcand/oxy buffer bug на больших телах) — откатил. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
248 lines
12 KiB
Markdown
248 lines
12 KiB
Markdown
---
|
||
title: oCIS на VDS — deploy recipe + non-obvious gotchas
|
||
type: concept
|
||
tags: [recipe, owncloud, ocis, docker, traefik, webdav, libregraph, decomposedfs]
|
||
sources: [../../.tasks/owncloud-vds-deploy.md]
|
||
updated: 2026-05-21
|
||
---
|
||
|
||
# oCIS на VDS — deploy recipe + non-obvious gotchas
|
||
|
||
Recipe для разворачивания **ownCloud Infinite Scale** (oCIS) — Go-based замены classic ownCloud Server 10 — в Docker за traefik с LetsEncrypt. Применимо на [[vds-kzntsv]] и других хостах с тем же pattern'ом (`/opt/stacks/<name>/`, traefik network `proxy`, LE certresolver `letsEncrypt`).
|
||
|
||
Validated 2026-05-21 на image `owncloud/ocis:7.1.0`. Container запустился, traefik routes 200 OK, LibreGraph API + WebDAV работают, init создаёт admin + idm.json в `data/idm/`.
|
||
|
||
## Когда применять
|
||
|
||
- Личный personal-cloud для одного пользователя (single binary, no external DB)
|
||
- Replace мёртвого / legacy ownCloud Server 10 (oCIS = main line от ownCloud GmbH, OC10 deprecating)
|
||
- Files-only use-case (calendar/contacts не нужны — для тех нужен Nextcloud)
|
||
- На хосте с уже работающим traefik + LE certresolver (recipe рассчитан на termination upstream)
|
||
|
||
## Когда **не** применять
|
||
|
||
- Multi-tenant корпоративная инсталляция → нужны external IDP, OIDC chain, не личный setup
|
||
- Нужны calendar / contacts / mail / collabora → Nextcloud, не oCIS
|
||
- Block-level dedup критичен → Seafile (oCIS использует file-level decomposedfs)
|
||
- Host UID = 1000 (image uid 1000 matches → `user:` override **не** нужен)
|
||
|
||
## Compose recipe
|
||
|
||
`/opt/stacks/owncloud/docker-compose.yml`:
|
||
|
||
```yaml
|
||
services:
|
||
ocis:
|
||
image: owncloud/ocis:7.1.0
|
||
container_name: owncloud
|
||
restart: unless-stopped
|
||
user: "1001:1001" # ← gotcha 1, см. ниже
|
||
entrypoint: /bin/sh
|
||
command: ["-c", "ocis init || true; exec ocis server"]
|
||
environment:
|
||
OCIS_URL: https://owncloud.kzntsv.site
|
||
OCIS_LOG_LEVEL: info
|
||
OCIS_LOG_COLOR: "false"
|
||
PROXY_TLS: "false" # ← gotcha 2
|
||
PROXY_ENABLE_BASIC_AUTH: "true" # ← gotcha 3
|
||
OCIS_INSECURE: "false"
|
||
IDM_CREATE_DEMO_USERS: "false"
|
||
IDM_ADMIN_PASSWORD: ${OCIS_ADMIN_PASSWORD}
|
||
volumes:
|
||
- ./data:/var/lib/ocis
|
||
- ./config:/etc/ocis
|
||
networks: [proxy]
|
||
labels:
|
||
- traefik.enable=true
|
||
- "traefik.http.routers.owncloud.rule=Host(`owncloud.kzntsv.site`)"
|
||
- traefik.http.routers.owncloud.entrypoints=websecure
|
||
- traefik.http.routers.owncloud.tls.certresolver=letsEncrypt
|
||
- traefik.http.services.owncloud.loadbalancer.server.port=9200
|
||
|
||
networks:
|
||
proxy: { external: true }
|
||
```
|
||
|
||
`/opt/stacks/owncloud/.env` (chmod 600):
|
||
```
|
||
OCIS_ADMIN_PASSWORD=<32+ random chars, store в pass owncloud/admin-password>
|
||
```
|
||
|
||
## Gotcha 1: UID mismatch image vs host
|
||
|
||
Image `owncloud/ocis` runs as **UID 1000** (`ocis-user` внутри контейнера). Если host-side user-owner mounted dirs ≠ 1000 — контейнер падает в restart-loop с:
|
||
|
||
```
|
||
Could not create config: open /etc/ocis/ocis.yaml: permission denied
|
||
The jwt_secret has not been set properly in your config for ocis.
|
||
```
|
||
|
||
«jwt_secret has not been set» — **обманка**, симптом сидит на ровном месте perm-deny при попытке записать ocis.yaml на первом `ocis init` пробеге.
|
||
|
||
**Fix:** `user: "<host-uid>:<host-gid>"` override в compose. На vds-kzntsv host user `vitya` = UID 1001 → `user: "1001:1001"`. Альтернатива (`chown 1000` host-dirs) **отвергнута**: создаёт orphan UID на хосте, путает auditing + backup tools.
|
||
|
||
**Diagnose UID:** `stat -c "%u:%g %U:%G" /opt/stacks/owncloud/config`.
|
||
|
||
## Gotcha 2: `PROXY_TLS=false` за reverse proxy
|
||
|
||
Дефолт oCIS — `PROXY_TLS=true` (HTTPS на proxy port 9200 self-signed). За traefik, который сам терминирует HTTPS, это даёт двойную TLS-обёртку — traefik пытается сделать backend TLS, не получается.
|
||
|
||
Per ownCloud docs: «If a reverse proxy is used to terminate HTTPS, `PROXY_TLS` can be set to false, though this means communication between the proxy and Infinite Scale will be unencrypted.» На том же хосте, в bridge network — acceptable.
|
||
|
||
## Gotcha 3: `PROXY_ENABLE_BASIC_AUTH=true` для WebDAV + admin API
|
||
|
||
Дефолтно oCIS — OIDC-only (browser-based login). Для **rclone / ownCloud-desktop / curl LibreGraph API** нужен basic auth — иначе 401 везде, включая `GET /graph/v1.0/me`.
|
||
|
||
**Trade-off:** basic auth не имеет MFA/refresh-tokens. Для personal use acceptable. Для shared deployment — обернуть LDAP / OIDC отдельно.
|
||
|
||
## Gotcha 4: User-create via LibreGraph API (нет CLI)
|
||
|
||
oCIS **не** имеет `ocis idm user add` или подобной CLI команды. Создание users — только через:
|
||
|
||
1. Web UI как admin → Settings → Users
|
||
2. LibreGraph API: `POST /graph/v1.0/users` с basic auth admin'а
|
||
|
||
API recipe:
|
||
```bash
|
||
curl -u "admin:$ADMIN_PWD" -X POST \
|
||
"https://owncloud.kzntsv.site/graph/v1.0/users" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"displayName": "Vitya",
|
||
"onPremisesSamAccountName": "vitya",
|
||
"mail": "vitya@kzntsv.site",
|
||
"passwordProfile": {"password": "<random-32>"},
|
||
"accountEnabled": true
|
||
}'
|
||
```
|
||
→ HTTP 201 с user JSON (с `id` UUID).
|
||
|
||
List users: `GET /graph/v1.0/users`.
|
||
|
||
## Storage layout (decomposedfs)
|
||
|
||
После `ocis init` data dir выглядит так:
|
||
|
||
```
|
||
/opt/stacks/owncloud/data/
|
||
├── idm/ — internal IDM (LDAP-style) state
|
||
├── idp/ — OIDC IdP private keys (private-key.pem etc)
|
||
├── nats/ — internal pub-sub messaging
|
||
├── search/ — indexer state (bleve)
|
||
└── storage/
|
||
├── metadata/spaces/<2>/<rest>/ — space metadata
|
||
└── users/spaces/<2>/<rest>/ — actual user files (decomposedfs nodes)
|
||
```
|
||
|
||
Каждый user space = `<2-char-prefix>/<26-char-rest-of-uuid>/` (split UUID для FS spread). Files внутри хранятся как **plain bytes** + sidecar metadata (xattrs / .meta.json). Backup-friendly: tar+rsync захватит всё корректно, восстановление = просто разархивировать обратно.
|
||
|
||
Internal KV-store (`storage/users/spaces/.../metadata/`, `idm/idm.json`, etc.) — **JSON files**, не БД. Это и есть «no external DB needed» — oCIS использует filesystem + NATS вместо Postgres/Redis.
|
||
|
||
## Backup integration
|
||
|
||
`/opt/stacks/owncloud` (whole dir) добавлен в rsync source list [[vds-backup-rsync-kreknin]]:
|
||
|
||
```diff
|
||
/opt/stacks/backup \
|
||
+ /opt/stacks/owncloud \
|
||
/etc/ssh \
|
||
```
|
||
|
||
Live container во время rsync — acceptable trade-off (decomposedfs использует atomic rename, отдельные node-files не corrupt'ятся). Если требуется strictly consistent snapshot — `docker stop owncloud` → rsync → `docker start` (downtime ~30s).
|
||
|
||
Первый full backup snapshot после import → +~25 GB на kreknin. Далее hardlink-incremental ≈ 1 GB/day.
|
||
|
||
## Connection chain
|
||
|
||
```
|
||
DNS owncloud.kzntsv.site → 89.253.255.94 (VDS_IP)
|
||
→ ufw 443
|
||
→ traefik websecure (LE cert via letsEncrypt resolver)
|
||
→ docker network proxy (172.18.0.0/16)
|
||
→ container owncloud:9200 (oCIS proxy service)
|
||
→ internal services: idp / idm / users / groups / proxy / storage-system / sharing / search ...
|
||
```
|
||
|
||
## Atomic revert
|
||
|
||
```bash
|
||
ssh vitya@89.253.255.94 'cd /opt/stacks/owncloud && docker compose down -v && cd .. && rm -rf owncloud'
|
||
# rsync source list cleanup (revert):
|
||
ssh vitya@89.253.255.94 "sed -i '\\|/opt/stacks/owncloud|d' /opt/stacks/backup/scripts/run.sh"
|
||
# secrets cleanup:
|
||
pass rm owncloud/admin-password owncloud/user-vitya && pass git push
|
||
```
|
||
|
||
## Gotcha 5: 60-секундный timeout на uploads через slow uplink
|
||
|
||
**Симптом:** rclone PUT любого файла, который не успевает залиться за 60 секунд через
|
||
клиентский uplink, падает с 502 (no buffering) или 500 (с buffering middleware).
|
||
Пороги при разной скорости uplink:
|
||
|
||
| Uplink | Cap |
|
||
|---|---|
|
||
| 3 MiB/s | ~180 MB |
|
||
| 10 MiB/s | ~600 MB |
|
||
| 50 MiB/s | ~3 GB |
|
||
|
||
В нашем кейсе (rclone 3.35 MiB/s) — упали все файлы ≥221 MB (4× .pat по 221-222 MB,
|
||
508 MB zip, 1.55 GB + 1.7 GB seospider).
|
||
|
||
**Где сидит 60s:** не traefik (default unlimited). Не на oCIS layer per env-var
|
||
(нет конфигурируемого ключа). Hardcoded где-то в HTTP stack — кандидаты:
|
||
|
||
- Go `http.Client.Timeout` в reva v2.27 datagateway.go:200 (видно в логах
|
||
oCIS: `Put "http://localhost:9158/data/simple/...": context canceled`,
|
||
`time_ns:59944479090` = 59.94s)
|
||
- rclone-side `http.Client.Timeout` (default Go = no timeout, но в библиотеке
|
||
WebDAV может быть явно установлен)
|
||
- HTTP/2 default stream timeout
|
||
|
||
Bumped via env-var не починить — закопано в коде reva. Apgrade oCIS image
|
||
до новой версии может помочь (newer reva имеет настраиваемый timeout) — defer.
|
||
|
||
**Что НЕ помогло:**
|
||
- `traefik.http.middlewares.<name>.buffering.maxRequestBodyBytes=0` +
|
||
`memRequestBodyBytes=1048576` — буферинг сам выдаёт 500 на 60s mark
|
||
(`backend_url="-"` в access log = traefik так и не доходит до backend).
|
||
Возможно ошибка в `vulcand/oxy` buffer на больших телах. Не разбирался —
|
||
workaround ниже проще.
|
||
|
||
**Workaround: VDS-side direct upload.**
|
||
|
||
Когда у нескольких файлов upload-time > 60s через клиентский uplink:
|
||
|
||
1. **scp с throwaway-key с restricted authorized_keys на VDS:** генерим одноразовый
|
||
ed25519 на VDS, append'им как `restrict,command="internal-sftp" <pub>`
|
||
в `~vitya/.ssh/authorized_keys` (sftp-only, no shell, no port forwards), private
|
||
отдаём агенту на slow-uplink машине. Cleanup ключа после.
|
||
|
||
2. **Агент `sftp -i <key> -b <batch>` пушит файлы в `/tmp/oc-import/`** на VDS.
|
||
SFTP через ssh не уязвим к 60s timeout — длинная сессия с keep-alive.
|
||
|
||
3. **C VDS-стороны: `curl -T <local-file> <ocis-url>` loopback'ом.**
|
||
Local network = >100 MB/s → 1.7 GB файл за 10 секунд, ≪ 60s timeout window.
|
||
PUT через traefik websecure (https://owncloud.kzntsv.site/dav/files/<user>/<path>)
|
||
с basic auth admin/user. URL-encode Cyrillic / spaces в path. Pattern:
|
||
|
||
```bash
|
||
ENC=$(python3 -c "import urllib.parse,sys; print('/'.join(urllib.parse.quote(p) for p in sys.argv[1].split('/')))" "$REMOTE_PATH")
|
||
curl -sS -u "vitya:$PWD" -T "$SRC" "https://owncloud.kzntsv.site/dav/files/vitya/${ENC}"
|
||
```
|
||
→ HTTP 201 Created.
|
||
|
||
4. **Verify через PROPFIND** — `<oc:size>` или `<d:getcontentlength>` должен
|
||
match'ить `stat -c %s` source.
|
||
|
||
5. **Cleanup:** `rm -rf /tmp/oc-import/`, remove throwaway-key line из
|
||
authorized_keys.
|
||
|
||
В нашем кейсе для 6 файлов / 4.04 GB local PUT cycle занял ~28 секунд total
|
||
(0.5-10s per file). vs rclone failed после ~5 минут на каждый retry × 5 retries × 6 файлов = безуспешно вечность.
|
||
|
||
## Related
|
||
|
||
- [[admin-infra-project]] — общий context для admin-infra repo
|
||
- [[vds-kzntsv]] — host entity (owncloud стек добавлен в его software stack)
|
||
- [[future-resilient-architecture-goals]] — RTO/RPO targets (oCIS — single-user, RTO/RPO defer)
|