Nextcloud Storage Node — Reference Deployment (Portainer Stack)
Status: operational reference · companion to
backend-architecture.md(§4, Nextcloud data plane) andoperations.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):
| Variable | Example Value | Description |
|---|---|---|
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_USER | admin (or custom name) | The initial Nextcloud admin account |
NEXTCLOUD_ADMIN_PASSWORD | (generate strong random) | Password for the initial admin account |
TUNNEL_TOKEN | eyJh... | 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
- 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 theappcontainer on port 80).
- Public hostname:
- 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=2Gconfigures 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 bysha256(ciphertext)(seebackend-architecture.md§4) — keep blob sizes under the per-request cap or chunk them. - 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 (seeoperations.md§7.7); backing up the node is defence in depth, not the sole recovery path.
5. Post-Install & Wiring FEN to This Backend
- Once the stack is running, go to
https://ncl.fenapp.netin your browser. - Because you provided
NEXTCLOUD_ADMIN_USERandNEXTCLOUD_ADMIN_PASSWORD, the install wizard is automatically skipped. The instance is fully provisioned. - Log in with your admin credentials.
- In Nextcloud, go to Settings → Security → Devices & sessions and create an App Password for FEN (do not reuse your login password).
- In the FEN app's backend chooser (Nextcloud is the pre-selected standard option — see
settings-ui.mdandbackend-architecture.md§4), supply:- Base URL:
https://ncl.fenapp.net - Username: your admin username
- App password: the one generated in step 4
- Base URL:
- Run the in-app Test connection. FEN probes
GET /ocs/v2.php/cloud/capabilitiesto confirm the server version and that public-link sharing + public upload are enabled (the common real-world blocker — seebackend-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.