Self-Hosted Cloud Storage
Private Nextcloud replacing Google Drive — cloud.tyfsadik.org
Overview
This project is a self-hosted Nextcloud instance deployed at cloud.tyfsadik.org that fully replaces Google Drive for file storage and synchronization. Nextcloud provides desktop sync clients for Linux and Windows, a mobile app for iOS and Android, and a web interface accessible from any browser.
The stack runs on Docker Compose with MariaDB as the relational backend for file metadata, Redis for in-memory caching and file-locking, and Nginx as the SSL-terminating reverse proxy. Files are stored on a dedicated Docker volume mounted from a ZFS dataset on the Proxmox host, giving snapshot-based backups for free. The deployment supports large file uploads up to 10 GB via chunked upload, and Redis caching reduces database queries for frequently accessed metadata, keeping the web UI responsive.
Architecture
Tech Stack
- Nextcloud — open-source file hosting with sync clients and web UI
- MariaDB — relational database for file metadata, shares, and users
- Redis — in-memory cache for APCu emulation and file-locking backend
- Nginx — reverse proxy with WebDAV support and large upload config
- Docker & Docker Compose — container orchestration
- Let's Encrypt / Certbot — TLS certificate for cloud.tyfsadik.org
- ZFS — underlying storage with snapshots for backup of the files volume
Build Process
Write docker-compose.yml
The compose file defines four services: Nextcloud (PHP-FPM), MariaDB, Redis, and Nginx. Named volumes are defined for the files data, MariaDB data, and Nginx config. All services are placed on a shared internal network, and only Nginx exposes ports 80 and 443 to the host.
version: "3.8"
services:
db:
image: mariadb:10.11
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: nextcloud
MYSQL_USER: ncuser
MYSQL_PASSWORD: ncpass
volumes:
- db_data:/var/lib/mysql
redis:
image: redis:7-alpine
restart: unless-stopped
app:
image: nextcloud:28-fpm-alpine
restart: unless-stopped
depends_on: [db, redis]
environment:
MYSQL_HOST: db
MYSQL_DATABASE: nextcloud
MYSQL_USER: ncuser
MYSQL_PASSWORD: ncpass
REDIS_HOST: redis
NEXTCLOUD_TRUSTED_DOMAINS: cloud.tyfsadik.org
volumes:
- nc_data:/var/www/html
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- nc_data:/var/www/html:ro
- ./certs:/etc/letsencrypt:ro
volumes:
db_data:
nc_data:
Nginx Config with Large File Upload Support
The Nginx configuration proxies PHP requests to the Nextcloud FPM container and raises the upload body size limit to support large file uploads. WebDAV methods are enabled for the sync clients. Headers required by Nextcloud (X-Frame-Options, X-Content-Type) are added for security compliance.
# nginx.conf (server block excerpt)
server {
listen 443 ssl;
server_name cloud.tyfsadik.org;
ssl_certificate /etc/letsencrypt/live/cloud.tyfsadik.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cloud.tyfsadik.org/privkey.pem;
# Large file upload support
client_max_body_size 10G;
client_body_timeout 300s;
fastcgi_read_timeout 300s;
root /var/www/html;
index index.php;
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Nextcloud required headers
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
}
Provision SSL Certificate
Certbot is run in webroot mode so it can validate domain ownership through the running Nginx container without stopping the server. The certificate is stored on the host and bind-mounted into the Nginx container.
apt install certbot -y
# Use webroot mode (Nginx handles /.well-known/acme-challenge)
certbot certonly --webroot \
-w /var/lib/docker/volumes/nextcloud_nc_data/_data \
-d cloud.tyfsadik.org \
--agree-tos --email [email protected]
# Renew hook to reload Nginx
echo '#!/bin/bash
docker exec nextcloud-nginx-1 nginx -s reload' \
> /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Nextcloud Initial Setup
After bringing the stack up, the Nextcloud installer is run via the web UI or
occ CLI to create the admin account and confirm the database connection.
Trusted domains are set so Nextcloud accepts requests from the public hostname.
docker compose up -d
# Run setup via CLI (recommended for automation)
docker exec --user www-data nextcloud-app-1 php occ maintenance:install \
--database mysql \
--database-host db \
--database-name nextcloud \
--database-user ncuser \
--database-pass ncpass \
--admin-user admin \
--admin-pass changeme \
--data-dir /var/www/html/data
# Add trusted domain
docker exec --user www-data nextcloud-app-1 php occ \
config:system:set trusted_domains 1 --value=cloud.tyfsadik.org
Install Sync Clients on Devices
The official Nextcloud desktop client is installed on Linux and Windows machines.
On mobile, the Nextcloud app is installed from the App Store / Play Store. Each client
is pointed to https://cloud.tyfsadik.org with the admin credentials.
Selective folder sync keeps only relevant directories synchronized locally.
# Linux desktop client (Debian/Ubuntu)
sudo apt install nextcloud-desktop
# Or AppImage from official releases:
wget https://github.com/nextcloud-releases/desktop/releases/latest/download/Nextcloud-x.x.x-x86_64.AppImage
chmod +x Nextcloud-*.AppImage && ./Nextcloud-*.AppImage
# Verify sync via CLI
nextcloudcmd -u admin -p pass /local/sync/folder https://cloud.tyfsadik.org
Configure Redis for Memory Caching
Redis is configured as both the APCu memory cache and the file-locking backend in
Nextcloud's config.php. This eliminates database-level file locks and
significantly improves performance when multiple clients sync simultaneously.
# Add to /var/www/html/config/config.php
# (edit inside the app container or on the bind-mounted config volume)
'memcache.local' => '\OC\Memcache\Redis',
'memcache.locking' => '\OC\Memcache\Redis',
'redis' => [
'host' => 'redis',
'port' => 6379,
'timeout' => 0.0,
],
# Apply config and run maintenance repair
docker exec --user www-data nextcloud-app-1 php occ maintenance:repair
docker exec --user www-data nextcloud-app-1 php occ db:add-missing-indices
Data Flow
Challenges & Solutions
-
Slow sync for large files: Syncing multi-gigabyte files stalled because the
default PHP upload limits (
upload_max_filesize = 2M) rejected the request. Resolved by settingclient_max_body_size 10Gin Nginx and configuring chunked uploads in Nextcloud so files are split into 10 MB pieces that each satisfy the PHP limit. -
Redis session locking causing timeout errors: During multi-client sync bursts,
file operations blocked waiting for Redis locks to expire. Fixed by setting
memcache.lockingto the Redis backend inconfig.php, which uses Redis atomic operations rather than slow database row locks. -
Nextcloud security scan warnings: The Nextcloud admin panel security scan
flagged missing HTTP headers and a missing
.well-knownredirect. Resolved by addingStrict-Transport-Security,X-Frame-Options, andX-Content-Type-Optionsheaders in Nginx, and adding rewrite rules for/.well-known/carddavand/.well-known/caldav. -
MariaDB running out of connections under load: The default MariaDB
max_connections=150was exhausted during large sync operations. Increased to 300 and added connection pooling via the Nextclouddbdriveroptionsconfig.
What I Learned
- PHP-FPM and Nginx FastCGI integration for PHP applications
- WebDAV protocol for file synchronization and its HTTP method extensions
- Redis as a distributed lock manager and cache layer in web applications
- MariaDB performance tuning: connection limits, query cache, InnoDB buffer pool
- Nextcloud
occCLI for administration, upgrades, and maintenance tasks - Chunked file upload mechanisms and how clients negotiate upload resumption