Day 1 — OpenCloud, proxy, and project web

OpenCloud on Docker Compose, Nginx with TLS on loopback, Fail2ban, cloud subdomain, and the KM0 Astro landing published as a second backend.

Introduction

Day 1 turns the Debian base into a full platform: OpenCloud on Docker Compose with official overlays, Nginx terminating TLS and routing only to loopback, coherent firewall policies, and the project’s Astro landing published as a second backend behind the same front door.

Post–first-cut improvements are also covered — Fail2ban and the dedicated cloud subdomain — because they are part of the real operational story of the deployment.

OpenCloud

Core without Collabora/WOPI

The chosen mode runs OpenCloud as a single service from the official composition (opencloud-eu/opencloud-compose), using a rolling image tagged (opencloud-rolling with a pinned tag at deploy time) so updates are deliberate (docker compose pull + maintenance window).

  • external-proxy overlay: adjusts variables such as PROXY_HTTP_ADDR to listen inside the container and publish the proxy HTTP port only as 127.0.0.1:<port> on the host.
  • COMPOSE_PROJECT_NAME=opencloud: anchors Docker volume names without depending on cwd.
  • .env file: single source of deploy variables; strict permissions on disk and outside version control.
  • COMPOSE_FILE: lists required overlays (base plus external-proxy overlay).

Inside the container, microservices coexist and communicate over gRPC/HTTP on internal localhost; that range is not exposed directly to the host except through endpoints defined by the upstream chart.

Architecture

OpenCloud behind the proxy

Browser
   │  HTTPS :443
   ▼
Nginx (Debian, dedicated OpenCloud site)
   │  HTTP http://127.0.0.1:9200  (loopback only)
   ▼
OpenCloud container (fixed UID/GID)
   │  internal microservices ~9140–9300
   ▼
Docker volumes:
   • opencloud-data   → files, indexes, NATS, IDM...
   • opencloud-config → opencloud.yaml, CSP, policies...

PROXY_TLS=false means TLS termination happens outside the container (on Nginx). OpenCloud generates coherent URLs when it receives correct X-Forwarded-* headers.

Ports

Exposed surface map

  • 22 (sshd): SSH administration — Internet per policy.
  • 80/443 (Nginx): public HTTP/S — ACME redirect and KM0 + OpenCloud virtual hosts.
  • 9200 (Docker → OpenCloud): 127.0.0.1 only — HTTP backend seen by Nginx.
  • 9140–9300: internal container microservices — not published on the host.
UFW reinforces the policy, allowing from the Internet only what is necessary. If the external browser should not know about it, it does not listen on all interfaces.

Nginx

Key directives toward OpenCloud

  • proxy_buffering off: SSE for real-time web client updates.
  • proxy_request_buffering off: resumable TUS uploads without buffering the entire body.
  • proxy_pass http://127.0.0.1:9200: TLS already handled at the edge.
  • X-Forwarded-Proto $scheme: coherent redirects and cookies for HTTPS.
  • Upgrade/Connection passthrough: WebSockets for the interactive UI.
  • Timeouts 3600s and client_max_body_size 10G: long sessions and large files.

Deployment

Tree at /opt/opencloud

/opt/opencloud/
├── opencloud-compose/     # upstream clone + overlays
│   ├── docker-compose.yml
│   ├── external-proxy/opencloud.yml
│   └── .env                 # active — outside git, chmod 600
├── nginx/                   # TLS + proxy templates
├── scripts/backup-volumes.sh
└── docs/runbook.md

Snippets in the repo serve as reference; active files under /etc/nginx/sites-available/ must always be checked with nginx -t before systemctl reload nginx.

Data

Docker volumes and persistence

OpenCloud centralizes persistence in two named volumes. Relevant content includes:

  • idm/ and idp/: internal LDAP directory and OIDC provider state.
  • nats/: JetStream event bus between microservices.
  • search/: full-text index (Bleve).
  • storage/: CS3 metadata and decomposed driver nodes.
  • web/: static assets for the integrated front end.

Encryption at rest: ordinary blobs within the volume; hardening options include LUKS, SSE on object backend, or E2E encryption in clients. Encryption in transit: TLS client↔Nginx.

KM0 web

Corporate site HTTPS flow

Internet :443 ─► Nginx host (TLS, km0digital.com)
                     └──► http://127.0.0.1:9180  (km0-web — loopback only)
                            Astro static + nginx Alpine
  • Stack: Astro 5 + Tailwind 3, static output.
  • i18n: JSON in src/i18n/ + routes /ca/, /en/, /de/; Spanish default at root.
  • Build: Node 22 Alpine multi-stage; repo at /opt/km0-web.
  • SEO: @astrojs/sitemap with hreflang alternates.

Perimeter

Fail2ban and cloud subdomain

After the first stable cut, Fail2ban was added as a complementary layer to the firewall. The cloud was published at cloud.km0digital.com, separate from the marketing brand at km0digital.com:

  • Certificates and CSP policies can diverge.
  • Users understand which URL to use for work vs communication.
  • Teams can delegate DNS/TLS without mixing static Astro configuration.

Operations

Routine commands

cd /opt/opencloud/opencloud-compose
docker compose ps
docker compose logs -f opencloud
docker compose pull && docker compose up -d
git -C /opt/opencloud/opencloud-compose pull

ss -tulpn | grep -E ‘:22|:80|:443|:9200’ ufw status verbose bash /opt/opencloud/scripts/backup-volumes.sh

Lab vs production

Deployment phases

  • Provisional TLS: self-signed certificate useful for validating the proxy — browser warnings until Let's Encrypt with stable DNS.
  • Domain: moving from raw IP to FQDN improves internal links and cookies.
  • Relaxed INSECURE: only coherent while internal certificates do not form a trusted PKI.
  • Backups: manual script until supervised cron; watch certbot.timer in production.

Next step

Day 2

Day 2 matures OIDC authentication with Dex, upgrades OpenCloud 7.x, and establishes the first full backup. In the meantime, explore the services or the day 2 story.