Skip to main content

Nextcloud Storage Node — Reference Deployment (Portainer Stack)

Status: operational reference · companion to backend-architecture.md (§4, Nextcloud data plane) and operations.md (§7.5 Backend Operator Guide).

Where this fits: Nextcloud is FEN's standard, recommended BYO storage backend — an S3‑compatible store (Garage/MinIO/AWS S3) is the alternative. This guide is the concrete, reproducible way a group owner self-hosts that standard backend. FEN ships no developer-provided backend; the store below is the owner's own and only ever sees ciphertext (see backend-architecture.md §1, Trust Model).

This stack deploys a robust Nextcloud storage backend on an Asustor NAS (using its standard /volume1 paths) and exposes it securely via a Cloudflare Tunnel at ncl.fenapp.net (an example hostname — substitute your own). It includes MariaDB for the database, Redis for file locking/caching, and a dedicated Cron container for background tasks. Once running, the owner points FEN's Nextcloud backend at it (base URL + admin username + an app password — see §5).

Security hardening baked in: authenticated Redis, fully configured trusted proxies for Cloudflare, strict healthcheck-gated startup for all services, pinned image versions, and reproducible admin provisioning.


1. Preparation: NAS Permissions

Before deploying, ensure the target directories exist on your Asustor NAS and have the correct ownership. Nextcloud runs as the www-data user (UID 33, GID 33). If the html folder is owned by root, the initial setup will crash with a permission error.

SSH into your NAS and pre-create the directories:

sudo mkdir -p /volume1/nextcloud/html
sudo mkdir -p /volume1/nextcloud/db
sudo mkdir -p /volume1/nextcloud/redis
# Assign ownership of the Nextcloud root to www-data (33:33)
sudo chown -R 33:33 /volume1/nextcloud/html

2. Portainer Setup

In your Portainer dashboard, create a new Stack and paste the following docker-compose.yml.

version: '3.8'

services:
db:
image: mariadb:10.11
restart: always
command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
volumes:
- ${NEXTCLOUD_DB_DIR:-/volume1/nextcloud/db}:/var/lib/mysql
networks:
- nextcloud-net
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5

redis:
image: redis:7-alpine
restart: always
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- ${NEXTCLOUD_REDIS_DIR:-/volume1/nextcloud/redis}:/data
networks:
- nextcloud-net
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5

app:
image: nextcloud:29-apache
restart: always
environment:
# Database
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_HOST=db
# Cache & Locking
- REDIS_HOST=redis
- REDIS_HOST_PASSWORD=${REDIS_PASSWORD}
# Reproducible Admin Provisioning (skips wizard)
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin}
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
# Cloudflare Tunnel / Trusted Proxy config
- NEXTCLOUD_TRUSTED_DOMAINS=ncl.fenapp.net
- TRUSTED_PROXIES=172.16.0.0/12 192.168.0.0/16 10.0.0.0/8 # Docker internal subnets
- APACHE_DISABLE_REWRITE_IP=1
- OVERWRITEHOST=ncl.fenapp.net
- OVERWRITEPROTOCOL=https
- OVERWRITECLIURL=https://ncl.fenapp.net
# PHP Limits for large file handling
- PHP_MEMORY_LIMIT=1024M
- PHP_UPLOAD_LIMIT=2G
volumes:
# Mounts the whole app root (code, config, data) correctly for the apache image
- ${NEXTCLOUD_HTML_DIR:-/volume1/nextcloud/html}:/var/www/html
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- nextcloud-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/status.php"]
interval: 15s
timeout: 5s
retries: 5
start_period: 5m

cron:
image: nextcloud:29-apache
restart: always
entrypoint: /cron.sh
environment:
- REDIS_HOST_PASSWORD=${REDIS_PASSWORD}
volumes:
- ${NEXTCLOUD_HTML_DIR:-/volume1/nextcloud/html}:/var/www/html
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- nextcloud-net

cloudflared:
image: cloudflare/cloudflared:2024.11.1
restart: unless-stopped
command: tunnel run
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN:?TUNNEL_TOKEN must be set in Portainer}
depends_on:
app:
condition: service_healthy
networks:
- nextcloud-net

networks:
nextcloud-net:
name: nextcloud-net

3. Environment Variables

Add the following variables in the Environment variables section of Portainer (or your .env file):

VariableExample ValueDescription
MYSQL_ROOT_PASSWORD(generate strong random)MariaDB root password
MYSQL_PASSWORD(generate strong random)Nextcloud database user password
REDIS_PASSWORD(generate strong random)Redis authentication password
NEXTCLOUD_ADMIN_USERadmin (or custom name)The initial Nextcloud admin account
NEXTCLOUD_ADMIN_PASSWORD(generate strong random)Password for the initial admin account
TUNNEL_TOKENeyJh...Cloudflare Zero Trust Tunnel token

⚠️ Important note on admin credentials: The NEXTCLOUD_ADMIN_USER and NEXTCLOUD_ADMIN_PASSWORD variables only provision the credentials on the very first run (initial database setup). If you try to change them here later, or remove the variables during a future redeploy, it will not update or delete the existing admin user in Nextcloud. Treat these strictly as a one-time bootstrap mechanism.


4. Cloudflare Routing & Caveats

  1. Routing: In your Cloudflare Zero Trust Dashboard → Networks → Tunnels → [Your Tunnel] → Public Hostname, map:
    • Public hostname: ncl.fenapp.net
    • Service: http://app:80 (this routes traffic from the tunnel directly into the app container on port 80).
  2. Upload cap note: Cloudflare Tunnels have a strict 100 MB per-request upload cap on their free/standard tiers. Nextcloud's web and desktop clients automatically chunk files into ~10 MB pieces to bypass this safely. The PHP_UPLOAD_LIMIT=2G configures the app-side ceiling to ensure assembled chunks are safely processed. If the FEN app ever pushes large individual payloads (e.g. huge receipt blobs), it must chunk uploads as well, otherwise Cloudflare will drop the connection. FEN encrypts attachments client-side and addresses them by sha256(ciphertext) (see backend-architecture.md §4) — keep blob sizes under the per-request cap or chunk them.
  3. Backup plan: Make sure your NAS backup utility (e.g. EZ Sync or a cron rsync) is backing up all three paths (/volume1/nextcloud/html, .../db, and .../redis) to ensure complete restoration capabilities. Note that FEN's own durability rests on the N member-device replicas of the encrypted log (see operations.md §7.7); backing up the node is defence in depth, not the sole recovery path.

5. Post-Install & Wiring FEN to This Backend

  1. Once the stack is running, go to https://ncl.fenapp.net in your browser.
  2. Because you provided NEXTCLOUD_ADMIN_USER and NEXTCLOUD_ADMIN_PASSWORD, the install wizard is automatically skipped. The instance is fully provisioned.
  3. Log in with your admin credentials.
  4. In Nextcloud, go to Settings → Security → Devices & sessions and create an App Password for FEN (do not reuse your login password).
  5. In the FEN app's backend chooser (Nextcloud is the pre-selected standard option — see settings-ui.md and backend-architecture.md §4), supply:
    • Base URL: https://ncl.fenapp.net
    • Username: your admin username
    • App password: the one generated in step 4
  6. Run the in-app Test connection. FEN probes GET /ocs/v2.php/cloud/capabilities to confirm the server version and that public-link sharing + public upload are enabled (the common real-world blocker — see backend-architecture.md §4.1). From then on, creating a group provisions a per-group folder + password-protected public link entirely in-app; members join account-lessly via the share token + password carried in their encrypted invite.