Nextcloud 34 on Docker: The Complete Hardened Setup Guide for Homelabs
Step-by-step Nextcloud 34 Docker deployment with PHP 8.4, Redis, and Let's Encrypt. Includes advanced security hardening, backup strategies, and fixes for the top 6 production errors.
Introduction
Self-hosting Nextcloud remains the gold standard for reclaiming ownership of your files, calendar, and contacts. The release of Nextcloud Hub 34 (v34.0.3, published 2026-08-13) introduces significant performance improvements in the Files app and a more robust federation API. However, a default docker-compose up without deliberate configuration will leave you with a slow, insecure instance vulnerable to brute-force attacks and data loss.
This guide walks you through a production-grade deployment using Docker Compose with a separate MariaDB database, Redis for distributed locking, and an optional Collabora container. You will learn the exact environment variables required, why the occ command is your best friend, and how to structure persistent volumes so that backups are trivial. We will also cover the most common misconfigurations that lead to 502 errors or corrupted file locks.
By the end, you will have a fully functional Nextcloud 34 instance on your homelab network, accessible via a reverse proxy with automatic SSL, configured with a proper cron job, and hardened against common web exploits. All commands are copy-paste ready, and every version is pinned to the verified release mentioned above. If you are migrating from an older version, note that the minimum supported PHP version is now 8.1, and the recommended web server is Apache 2.4.
This article assumes you are comfortable with the Linux command line and basic Docker concepts. We will not cover basic Docker installation, but we will provide a checklist to verify your environment before starting.
Prerequisites / Requirements
Before you begin, ensure your homelab server meets the following minimum specifications. These are typical ranges based on community reports, not official benchmarks. Actual usage depends heavily on the number of concurrent users, the size of your file library, and whether you enable preview generation or full-text search.
| Component | Minimum Requirement | Recommended | Notes |
|---|---|---|---|
| CPU | 2 cores | 4+ cores | The preview and cron jobs are CPU-intensive. ARM (like Raspberry Pi 4) works but will be slower for video previews. |
| RAM | 4 GB | 8 GB | PHP-FPM and MariaDB will consume 1.5-2 GB together. Redis adds another 256 MB. Do not allocate less than 2 GB to the PHP container. |
| Storage | 50 GB free | 1 TB+ | Use a separate volume for /var/www/html/data. NVMe drives drastically improve database response times. |
| OS | Ubuntu 22.04 LTS | Debian 12 / Ubuntu 24.04 | Any modern Linux distribution works. Ensure your kernel supports overlay2 (default). |
| Software | Docker Engine 24+ | Docker Engine 27+ | Verify with docker --version. |
| Software | Docker Compose v2 | Docker Compose v2.24+ | Verify with docker compose version. |
| Network | Static IP or domain | Domain name pointing to your server | Required for Let's Encrypt. For LAN-only, you can skip the reverse proxy but must use HTTP. |
| Time | NTP synchronized | chrony | Mismatched clocks cause JWT validation failures in federated sharing. |
Important Verification Step: Run id -u && id -g on your host. The official Nextcloud image runs Apache as user www-data (UID 33) inside the container. When you bind-mount volumes, the host directory must be writable by UID 33. If your host user has a different UID (e.g., 1000), you must either change the directory ownership to 33:33 or run the container with user: "${PUID:-33}:${PGID:-33}" and adjust the .env file accordingly. We will use the latter approach for flexibility.
Step-by-Step Installation Guide
Step 1: Create the Project Directory and .env File
Create a dedicated directory for your stack. We will use ~/nextcloud-docker. All configuration files will live here.
mkdir -p ~/nextcloud-docker && cd ~/nextcloud-docker
Now, create a .env file inside this directory. This file will store all your secrets and configurable values. Never commit this file to Git. Add it to your .gitignore if you use version control.
cat > .env <<'EOF'
# Database Configuration
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
MYSQL_PASSWORD=change_this_strong_db_password
MYSQL_ROOT_PASSWORD=change_this_strong_root_password
# Nextcloud Admin User (created on first run)
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=change_this_strong_admin_password
# Host paths for persistent data - adjust if needed
NEXTCLOUD_DATA_DIR=./data
NEXTCLOUD_CONFIG_DIR=./config
NEXTCLOUD_APPS_DIR=./apps
# User/Group IDs for file permissions - run 'id -u' and 'id -g' to check
PUID=33
PGID=33
# Timezone for cron jobs
timezone=UTC
EOF
Set strict permissions on this file:
chmod 600 .env
Step 2: Create the Docker Compose File
We will use the official nextcloud:34.0.3-apache image. The apache tag is preferred over fpm for simplicity, as it includes the web server and PHP in one container, reducing the number of moving parts for a homelab. We will add Redis and MariaDB as separate services.
Create a file named docker-compose.yml in the same directory. Copy the entire block below. Do not omit the command: section, as it configures the background cron job.
services:
db:
image: mariadb:11.4
restart: unless-stopped
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW --innodb-file-per-table=1 --skip-innodb-read-only-compressed
volumes:
- db_data:/var/lib/mysql
environment:
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_HOST_PASSWORD:-}
volumes:
- redis_data:/data
app:
image: nextcloud:34.0.3-apache
restart: unless-stopped
ports:
- "8080:80"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
volumes:
- ${NEXTCLOUD_DATA_DIR}:/var/www/html
environment:
- MYSQL_HOST=db
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- REDIS_HOST=redis
- REDIS_HOST_PASSWORD=${REDIS_HOST_PASSWORD:-}
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
- NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS:-localhost}
- PHP_MEMORY_LIMIT=512M
- PHP_UPLOAD_LIMIT=10G
command: |
bash -c "apache2-foreground &
while ! nc -z db 3306; do sleep 1; done;
while ! nc -z redis 6379; do sleep 1; done;
php /var/www/html/occ config:system:set memcache.local --value '\\OC\\Memcache\\Redis' &&
php /var/www/html/occ config:system:set memcache.distributed --value '\\OC\\Memcache\\Redis' &&
php /var/www/html/occ config:system:set memcache.locking --value '\\OC\\Memcache\\Redis' &&
php /var/www/html/occ config:system:set redis host --value 'redis' &&
php /var/www/html/occ config:system:set redis port --value '6379' --type=integer &&
php /var/www/html/occ config:system:set redis password --value '${REDIS_HOST_PASSWORD:-}' &&
php /var/www/html/occ config:system:set trusted_domains 1 --value '${NEXTCLOUD_TRUSTED_DOMAINS:-localhost}' &&
crontab -u www-data -l 2>/dev/null; echo '*/5 * * * * php -f /var/www/html/cron.php' | crontab -u www-data - &&
cron -f"
volumes:
db_data:
redis_data:
Note on the command block: The official image entrypoint initializes the database on first run. The command we append runs after the entrypoint script. It waits for the database and Redis to be reachable, then sets the Redis cache configuration and the cron job. The nc command is available in the image. If you have a different setup, verify the image documentation.
Step 3: Launch the Stack
From the ~/nextcloud-docker directory, run:
docker compose up -d
This command will pull the images and start the containers in detached mode. Wait a few minutes for the database to initialize. You can monitor the logs with:
docker compose logs -f app
You should see a message indicating that the installation was successful. If you see errors about connection refused, wait 30 seconds and try again, as the database might still be starting.
Step 4: Complete the Web Setup Wizard
Open your browser and navigate to http://your-server-ip:8080. You will see the Nextcloud setup page. Enter the admin username and password you defined in the .env file. Leave the database settings as they are (they are pre-configured via environment variables). Click "Install".
After installation, you will be logged into your new Nextcloud instance. The data directory is already configured to /var/www/html/data which is inside the mounted volume.
Step 5: Configure the Background Jobs (Cron)
We have already set up a cron job in the command section of the compose file. To verify it is running, execute the following inside the app container:
docker compose exec -u www-data app php /var/www/html/cron.php
You should see no output, and the exit code should be 0. To check the cron configuration in Nextcloud, go to Settings > Basic settings and ensure Cron is selected as the background job. The command we added runs every 5 minutes.
Step 6: Verify Redis Cache is Active
Redis caching is critical for performance. To confirm it is working, run:
docker compose exec -u www-data app php /var/www/html/occ status
The output should show installed: true. To specifically check Redis, look at the Nextcloud log file:
docker compose exec app tail -n 50 /var/www/html/data/nextcloud.log | grep -i redis
If you see no errors, Redis is active. You can also run occ config:list system and look for memcache.local set to OC\Memcache\Redis.
Step 7: Set Up a Reverse Proxy with SSL (Recommended)
While accessing via port 8080 works, you should put Nextcloud behind a reverse proxy like Nginx or Caddy for SSL termination and better security. We will use Caddy for its automatic HTTPS and simple configuration.
First, stop the app container from exposing its port directly. Edit docker-compose.yml and remove the ports section from the app service. Then, add a new service for Caddy.
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- app
volumes:
caddy_data:
caddy_config:
Create a Caddyfile in the same directory with the following content. Replace cloud.example.com with your actual domain name. Ensure your DNS A record points to your server's IP.
cloud.example.com {
reverse_proxy app:80
}
Now, recreate the containers:
docker compose up -d --force-recreate
Caddy will automatically obtain a Let's Encrypt certificate for your domain. Access your Nextcloud instance via https://cloud.example.com.
Step 8: Set Trusted Domains
If you access Nextcloud via the reverse proxy, you must add the domain to the trusted domains list. We already set NEXTCLOUD_TRUSTED_DOMAINS in the environment, but you may need to add more. Run:
docker compose exec -u www-data app php /var/www/html/occ config:system:set trusted_domains 2 --value=cloud.example.com
Replace cloud.example.com with your domain. If you need to access via IP as well, add another entry.
Advanced Setup and Optimization
File Locking and Transactional File Locking
With Redis configured, transactional file locking is enabled by default. To verify, check the config file:
docker compose exec app cat /var/www/html/config/config.php | grep -i "filelocking"
If 'filelocking.enabled' => true is not present, add it via occ:
docker compose exec -u www-data app php /var/www/html/occ config:system:set filelocking.enabled --value=true --type=boolean
Backup Strategy
The most important part of any homelab service is backups. For Nextcloud, you need to backup three things: the database, the config/ directory, and the data/ directory. The apps/ directory can be recovered by re-running occ app:install but it is easier to back it up as well.
We will create a simple backup script that uses mysqldump and tar. Create a file named backup.sh in your project directory and make it executable.
#!/bin/bash
# Backup script for Nextcloud
set -euo pipefail
BACKUP_DIR="./backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# Backup the database
docker compose exec -T db mysqldump --single-transaction -u ${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE} > "$BACKUP_DIR/db_$TIMESTAMP.sql"
# Backup the config and data directories (excluding the database files)
tar -czf "$BACKUP_DIR/nextcloud_$TIMESTAMP.tar.gz" \
-C ./ \
config data apps
echo "Backup completed to $BACKUP_DIR"
Run chmod +x backup.sh and execute it with ./backup.sh. Schedule this script in your host's crontab to run daily.
Security Hardening (Optional Hardening)
The following settings are considered advanced. They may break your setup if copied blindly. Test them in a staging environment first. The official Nextcloud documentation recommends these for internet-facing instances.
- Set
overwriteprotocoltohttpsso that Nextcloud generates correct links behind the proxy.docker compose exec -u www-data app php /var/www/html/occ config:system:set overwriteprotocol --value=https - Enable
htaccess.RewriteBasefor Apache to handle URL rewriting properly.docker compose exec -u www-data app php /var/www/html/occ config:system:set htaccess.RewriteBase --value='/' - Add security headers via a custom
.htaccessor in your reverse proxy. In Caddy, you can add headers likeX-Content-Type-Options: nosniffandX-Frame-Options: SAMEORIGIN. - Run containers with read-only root filesystem and drop all capabilities except what is needed. This is complex and requires careful mapping of writable directories. The official image needs
/var/www/htmlto be writable for the session files and apps. If you attempt this, you must mount additional tmpfs volumes for/tmp,/var/www/html/sessions, and/var/www/html/custom_apps. We do not recommend this for a standard homelab due to the high chance of breaking updates.
Troubleshooting Common Errors
| Error | Cause | Solution |
|---|---|---|
| 502 Bad Gateway from reverse proxy | The app container is not running or is unhealthy. | Check docker compose ps and docker compose logs app. The most common cause is a database connection failure. Ensure the db container is healthy. |
SQLSTATE[HY000] [2002] Connection refused |
The app container started before the database was ready. | Our command block includes a wait loop. If you removed it, restart the stack with docker compose restart. Increase the interval in the healthcheck. |
Redis connection refused |
The Redis container is not running or the password is wrong. | Check docker compose ps redis. Ensure the REDIS_HOST_PASSWORD variable is set in .env and matches the command in the redis service. |
| Files are locked after a crash | The transactional file locking is disabled or Redis lost the lock data. | Make sure Redis is persistent (we used a volume). Run occ files:scan --all to fix file integrity, but the locks should clear automatically after the lock timeout (default 60 minutes). |
HTTP 413 Request Entity Too Large |
PHP upload limit is too low. | We set PHP_UPLOAD_LIMIT=10G in the environment. If you changed it or are using a reverse proxy, check the proxy's client_max_body_size. In Caddy, add request_body { max_size 10GB } to the site block. |
Cannot write into apps directory |
The apps directory is not writable by the web server user (UID 33). |
Run sudo chown -R 33:33 apps config data on the host. This is the most common permission error. |
| Cron job not running | The cron binary is not installed or the crontab is empty. |
Exec into the container and run crontab -u www-data -l. If empty, re-run the command from Step 2. Ensure the cron package is installed in the image (it is). |
Conclusion and FAQ
Deploying Nextcloud 34 with Docker Compose is straightforward, but doing it correctly requires attention to detail. By using a separate database, Redis for caching, and a proper cron job, you have built a solid foundation that can handle dozens of users and terabytes of data. The reverse proxy setup with Caddy gives you automatic SSL and removes the need to expose the application port directly. Regular backups using the script provided will save you from disaster.
The configuration we have provided is not the absolute maximum hardening possible, but it is the sweet spot for a homelab where maintainability and reliability are key. Remember to check the official Nextcloud Admin Manual for any changes in future releases, as the configuration options can evolve.
FAQ
1. Can I update Nextcloud to a newer version without recreating the container?
Yes, but you should not just run docker compose pull. The official image supports running occ upgrade after the new image is pulled. First, backup your database and data. Then, change the image tag in docker-compose.yml and run docker compose up -d. The entrypoint will detect the new version and run the upgrade scripts automatically. Always test on a staging copy first.
2. Why is my Nextcloud slow even with Redis?
Redis solves the caching bottleneck, but the database is often the next constraint. Ensure MariaDB has enough memory allocated (check innodb_buffer_pool_size). Also, disable the preview app if you do not need image thumbnails, as it is CPU-intensive. You can disable it with occ app:disable preview. Finally, check that your storage is not a slow HDD; using an SSD for the database volume makes a significant difference.
3. How do I move Nextcloud to a different server?
The process is: stop the containers, backup the database and the config, data, and apps directories. On the new server, set up the same Docker Compose file and .env file. Restore the directories and the database dump. Start the containers. The entrypoint will detect the existing config and skip the installation wizard. Run occ maintenance:mode --off if it was left on.
4. Is it safe to expose Nextcloud directly to the internet without a reverse proxy? Technically yes, but it is strongly discouraged. The built-in Apache server is not designed to handle malicious traffic directly. A reverse proxy like Caddy or Nginx adds a layer of protection, allows you to set security headers, and simplifies SSL certificate management. It also enables you to run multiple services on port 443 with different domain names.
5. What is the recommended way to handle file access via WebDAV?
Nextcloud exposes WebDAV at /remote.php/dav/. For desktop clients, you do not need to configure anything; the client uses this endpoint automatically. If you are using a third-party app, ensure you use the full URL: https://cloud.example.com/remote.php/dav/. For performance, consider enabling HTTP/2 on your reverse proxy, which significantly speeds up many small file transfers.