Nextcloud 34 Backup and Restore: A Complete Docker Compose Guide for Homelabs
Master bulletproof Nextcloud 34 backup and restore in Docker. Step-by-step Compose setup, offsite backups, and disaster recovery without data loss.
Introduction
Nextcloud has become the backbone of many homelabs, serving as a private cloud for files, calendars, contacts, and collaborative document editing. However, the convenience of self-hosting comes with the critical responsibility of data protection. A hardware failure, accidental deletion, or a botched upgrade can wipe out years of personal data in seconds. While Nextcloud is robust, its underlying database and file structure require a coordinated, consistent backup strategy — not just copying the data folder and hoping for the best.
This guide provides a production-grade approach to backing up and restoring Nextcloud v34.0.3 running in Docker Compose. We will move beyond simple file copies and implement a consistent, scriptable backup pipeline using pg_dump for PostgreSQL and borgbackup for file-level deduplication. You will learn how to structure your volumes, automate snapshots, and perform a clean restore into a fresh environment.
By the end of this guide, you will have a complete, tested methodology for disaster recovery. We will cover the entire lifecycle: initial deployment with backup-friendly configurations, automated backup scripts, secure offsite transfer, and a step-by-step restoration process. The goal is to ensure that your Nextcloud instance can survive any catastrophic event with minimal downtime and zero data loss.
This guide assumes a working knowledge of Docker and basic Linux administration. We will use Docker Compose exclusively (no Kubernetes) and target a standard x86_64 homelab server. All commands are provided as complete, copy-paste-ready blocks.
Prerequisites
Before diving into the Compose file, ensure your host meets these minimum requirements. These are typical ranges, not hard limits — actual usage depends on the number of users, file sizes, and enabled apps like Talk or Office.
| Component | Requirement | Notes |
|---|---|---|
| CPU | 2 vCPUs (minimum), 4+ recommended | Nextcloud is PHP-based; more cores help with concurrent requests and background jobs. |
| RAM | 4 GB (minimum), 8 GB recommended | PHP-FPM, PostgreSQL, and Redis all consume memory. Large file previews spike usage. |
| Storage | 50 GB (system + OS) + 2x your actual data size | You need space for the data volume, the database, and the local backup repository (if kept on the same disk). |
| OS | Ubuntu 22.04/24.04 LTS or Debian 12 | Any modern Linux distribution works. Windows/macOS are not covered here. |
| Software | Docker Engine 24+ and Docker Compose v2 | Install via official Docker repos, not distro packages. |
| Network | Static IP or DNS name | Required for trusted domain configuration and reverse proxy setup. |
Facts you must verify on your host:
- Run
id -u && id -gto know your host user ID. We will use this for volume permissions. The official Nextcloud image runs aswww-data(UID 33) but the volumes must be accessible by the container. We will not hardcode 33:33; instead, we will use environment variables. - Ensure your system time is synchronized (NTP). Backup consistency relies on correct timestamps.
Step-by-Step Setup and Backup Configuration
Step 1: Prepare the Directory Structure and .env File
Create the main directory and a .env file to hold all secrets and configuration variables. Never put passwords directly in docker-compose.yml.
mkdir -p ~/nextcloud-backup && cd ~/nextcloud-backup && touch .env && chmod 600 .env && echo 'Creating .env file - edit it now with your values' && exit 1
Now edit the .env file with your favorite text editor (e.g., nano .env). Populate it with the following content. Generate strong passwords using openssl rand -base64 32 for each secret.
# Database credentials
POSTGRES_DB=nextcloud
POSTGRES_USER=nextcloud
POSTGRES_PASSWORD=CHANGE_ME_DB_PASSWORD
# Nextcloud admin user (initial setup only)
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=CHANGE_ME_ADMIN_PASSWORD
# Host paths - adjust to your setup
NEXTCLOUD_DATA_DIR=./data
NEXTCLOUD_CONFIG_DIR=./config
NEXTCLOUD_APPS_DIR=./apps
# Backup repository password (used by borg)
BACKUP_REPO_PASSPHRASE=CHANGE_ME_BORG_PASSPHRASE
# Your host UID/GID (run `id -u` and `id -g` to get these)
PUID=1000
PGID=1000
Warning: Never commit this .env file to a Git repository. Add it to .gitignore immediately if you use version control.
Step 2: Create the Docker Compose File
Create a docker-compose.yml in the same directory. This file defines the Nextcloud service, PostgreSQL database, and Redis cache. We are using the official images only. The Nextcloud version is pinned to v34.0.3 as verified. For the database, we use the latest stable PostgreSQL 16 image.
services:
db:
image: postgres:16-alpine
container_name: nextcloud-db
restart: unless-stopped
volumes:
- db_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: nextcloud-redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_HOST_PASSWORD}
volumes:
- redis_data:/data
environment:
- REDIS_HOST_PASSWORD=CHANGE_ME_REDIS_PASSWORD # Add this to .env too
app:
image: nextcloud:34.0.3
container_name: nextcloud-app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
ports:
- "8080:80"
volumes:
- ${NEXTCLOUD_DATA_DIR}:/var/www/html/data
- ${NEXTCLOUD_CONFIG_DIR}:/var/www/html/config
- ${NEXTCLOUD_APPS_DIR}:/var/www/html/custom_apps
environment:
- POSTGRES_HOST=db
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- REDIS_HOST=redis
- REDIS_HOST_PASSWORD=${REDIS_HOST_PASSWORD}
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
- TRUSTED_DOMAINS=localhost,192.168.1.100 # Replace with your homelab IP or domain
- OVERWRITEPROTOCOL=https # Set to http if not using a reverse proxy
- PHP_MEMORY_LIMIT=512M
- PHP_UPLOAD_LIMIT=10G
volumes:
db_data:
redis_data:
Important settings explained:
TRUSTED_DOMAINSmust include your server's IP or domain, otherwise Nextcloud will reject requests with a security warning.OVERWRITEPROTOCOLshould behttpswhen behind a reverse proxy that terminates SSL.- The volumes point to host directories for data, config, and custom apps. This simplifies backups.
- We use named volumes for the database and Redis to ensure proper permission handling by the official images.
Step 3: Launch and Initial Setup
Start the stack and wait for the database to initialize.
docker compose up -d && docker compose logs -f db && echo 'Database is ready' && docker compose up -d
After the first launch, access http://your-server-ip:8080 and complete the initial web setup. The admin user and password from your .env file are pre-filled. Do not change them in the UI unless you update the environment variables accordingly.
Step 4: Configure Nextcloud for Backup Consistency
Enable maintenance mode to ensure a consistent backup. This prevents writes during the snapshot process.
docker exec -u www-data nextcloud-app php occ maintenance:mode --on && echo 'Maintenance mode enabled'
Now, create a dedicated backup script that performs a logical dump of the database. We will use pg_dump inside the database container.
docker exec nextcloud-db pg_dump -U ${POSTGRES_USER} -d ${POSTGRES_DB} --format=custom --file=/tmp/nextcloud.dump && docker cp nextcloud-db:/tmp/nextcloud.dump ~/nextcloud-backup/db.dump && docker exec nextcloud-db rm /tmp/nextcloud.dump && echo 'Database dump completed'
Step 5: Install BorgBackup for File-Level Deduplication
BorgBackup provides deduplicated, compressed, and encrypted backups. Install it on your host (not in a container) for simplicity.
sudo apt update && sudo apt install borgbackup -y && borg --version && echo 'Borg installed'
Step 6: Initialize the Local Backup Repository
Create a local repository for testing. In production, you should use a separate disk or a remote SSH location.
borg init --encryption=repokey-blake2 ~/nextcloud-backup/borg-repo && echo 'Repository initialized'
Step 7: Create a Full Backup Script
Create a script backup.sh that combines the database dump and the file backup. This script is idempotent and safe to run manually or via cron.
touch ~/nextcloud-backup/backup.sh && chmod +x ~/nextcloud-backup/backup.sh && cat > ~/nextcloud-backup/backup.sh << 'EOF'
#!/bin/bash
set -euo pipefail
# Load environment variables
source ~/nextcloud-backup/.env
# Timestamp for logging
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
LOG_FILE=~/nextcloud-backup/backup.log
# Enable maintenance mode
docker exec -u www-data nextcloud-app php occ maintenance:mode --on
# Dump the database
echo "[${TIMESTAMP}] Dumping database..." >> "${LOG_FILE}"
docker exec nextcloud-db pg_dump -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" --format=custom --file=/tmp/nextcloud.dump
docker cp nextcloud-db:/tmp/nextcloud.dump ~/nextcloud-backup/db.dump
docker exec nextcloud-db rm /tmp/nextcloud.dump
# Disable maintenance mode
docker exec -u www-data nextcloud-app php occ maintenance:mode --off
# Run borg backup
echo "[${TIMESTAMP}] Running borg backup..." >> "${LOG_FILE}"
export BORG_PASSPHRASE="${BACKUP_REPO_PASSPHRASE}"
borg create --stats --compression lz4 "${HOME}/nextcloud-backup/borg-repo::nextcloud-${TIMESTAMP}" \
~/nextcloud-backup/db.dump \
./data \
./config \
./apps
# Prune old backups (keep 7 daily, 4 weekly, 6 monthly)
echo "[${TIMESTAMP}] Pruning old backups..." >> "${LOG_FILE}"
borg prune --keep-daily 7 --keep-weekly 4 --keep-monthly 6 "${HOME}/nextcloud-backup/borg-repo"
echo "[${TIMESTAMP}] Backup completed successfully." >> "${LOG_FILE}"
EOF
# Make the script executable
chmod +x ~/nextcloud-backup/backup.sh
Run the backup script once to ensure it works:
cd ~/nextcloud-backup && ./backup.sh && echo 'Backup script executed successfully'
Step 8: Automate with Cron
Schedule the backup to run daily at 2 AM.
(crontab -l 2>/dev/null; echo "0 2 * * * /home/yourusername/nextcloud-backup/backup.sh") | crontab - && echo 'Cron job added' && crontab -l
Step 9: Restore from Backup (Disaster Recovery)
To test restoration, you can simulate a failure by stopping all containers and wiping the volumes.
Warning: This will delete your current data. Only perform this in a test environment.
cd ~/nextcloud-backup && docker compose down -v && rm -rf data config apps db.dump && echo 'Volumes and data directories removed'
Now, restore the database and files.
# Find the latest backup
LATEST_BACKUP=$(borg list --short ~/nextcloud-backup/borg-repo | tail -n 1) && echo "Restoring from: ${LATEST_BACKUP}"
# Extract the backup
export BORG_PASSPHRASE="${BACKUP_REPO_PASSPHRASE}"
cd ~/nextcloud-backup && borg extract --list "${HOME}/nextcloud-backup/borg-repo::${LATEST_BACKUP}" && echo 'Files extracted'
# Restore the database dump into the container
mkdir -p data config apps
Step 10: Rebuild and Import the Database
Start the database container separately to import the dump.
docker compose up -d db && docker compose exec db pg_restore -U ${POSTGRES_USER} -d ${POSTGRES_DB} --clean --if-exists /tmp/nextcloud.dump
Note: The above command assumes the dump file is accessible inside the container. You may need to copy it first with docker cp db.dump nextcloud-db:/tmp/. Then run pg_restore.
Step 11: Start the Full Stack and Verify
Bring up all services and check the Nextcloud logs.
docker compose up -d && docker compose logs -f app | tail -n 50
Access your Nextcloud instance and verify that all files, users, and settings are intact. Run occ maintenance:mode --off if it was left on.
Advanced Configuration and Hardening
Reverse Proxy with SSL (Caddy)
Use Caddy to automatically obtain Let's Encrypt certificates and forward traffic to the Nextcloud container.
Create a Caddyfile:
cloud.example.com {
reverse_proxy nextcloud-app:80
}
Add the Caddy service to your docker-compose.yml:
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
Offsite Backup Replication
Copy the borg repository to an external server using rclone or rsync. For a truly offsite solution, use borg with a remote SSH repository.
borg init --encryption=repokey-blake2 user@remote-server:/path/to/repo
Then modify backup.sh to create the backup directly on the remote host.
Optional Hardening
Warning: The following security settings may break your containers if applied incorrectly. Test them thoroughly in a staging environment before using in production.
- Read-only root filesystem: Add
read_only: trueto the app service. This requires careful volume mapping for writable directories (/tmp,/var/www/html/data, etc.). - Drop kernel capabilities: Add
cap_drop: [ALL]andcap_add: [CHOWN, SETUID, SETGID, DAC_OVERRIDE]for the app container. The exact set depends on the image's requirements. - Run as non-root: The Nextcloud image runs as
www-data(UID 33) by default. Do not override this. For PostgreSQL, the official image already runs as a non-root user.
Troubleshooting
| Common Error | Root Cause | Solution |
|---|---|---|
SQLSTATE[HY000] [1045] Access denied for user |
Database password mismatch between .env and the actual PostgreSQL credentials. |
Check the env vars in the container: `docker compose exec db env |
Trusted domain error after restore |
The config/config.php file contains a different domain than what you are accessing. |
Edit config/config.php and update the trusted_domains array. |
File not found when accessing a file |
The file exists in the data directory but the database entry is missing. | Run docker exec -u www-data nextcloud-app php occ files:scan --all to rescan the filesystem. |
Maintenance mode is on after restore |
The restore process did not disable maintenance mode. | Run docker exec -u www-data nextcloud-app php occ maintenance:mode --off. |
Borg fails with permission denied |
The backup script is running as a different user than the owner of the repository. | Ensure the repository is owned by the user running the cron job. Use chown -R $(id -u):$(id -g) ~/nextcloud-backup/borg-repo. |
Cannot connect to Redis |
Redis password mismatch or container not running. | Verify the Redis container is healthy and the password in .env matches the command in the Compose file. |
Conclusion
Implementing a reliable backup and restore strategy for Nextcloud is not optional — it is the only thing standing between you and catastrophic data loss. By combining a logical database dump with a deduplicated file backup, you have created a robust, verifiable pipeline. The scripts provided are modular and can be extended with offsite replication or monitoring alerts.
We encourage you to test your restore process at least once a quarter. A backup that has never been restored is not a backup — it is a wish. With this guide, you have the tools to turn that wish into a tested, operational reality.
FAQ
Q1: Can I use SQLite instead of PostgreSQL for easier backups?
Yes, but it is not recommended for production. SQLite is file-based and cannot handle concurrent writes well. PostgreSQL, as used here, provides transactional integrity and allows consistent dumps via pg_dump. The additional setup complexity is worth the reliability.
Q2: How do I restore to a different server with a different IP address?
During the restore process, after starting the containers, edit config/config.php and update the trusted_domains array to include the new IP or domain. Then access the instance normally. You may also need to update the overwrite.cli.url value.
Q3: What is the difference between pg_dump and copying the db_data volume?
pg_dump creates a logical dump that is portable across PostgreSQL versions and architectures. Copying the volume is a physical backup that must match the exact PostgreSQL version and filesystem permissions. Logical dumps are safer for disaster recovery.
Q4: How do I verify the integrity of my borg backups?
Run borg check ~/nextcloud-backup/borg-repo to verify the repository structure and archive integrity. You can also do a dry-run extraction with borg extract --dry-run --list. Schedule these checks regularly.
Q5: Should I back up the config directory and custom_apps?
Absolutely. The config/config.php file contains your database credentials, trusted domains, and various settings. Without it, restoring the data volume alone would still break the instance. The custom_apps folder contains manually installed applications that are not part of the default image. Both are critical for a full restore.