Nextcloud is the gold standard for self-hosted cloud storage. It’s like having your own Google Drive, Google Calendar, and Google Contacts — but you own all the data. In this guide, I’ll walk you through setting up Nextcloud on Docker from scratch, including the reverse proxy, SSL certificates, and crucial performance optimizations that most guides skip.

By the end, you’ll have a production-ready Nextcloud instance that’s fast, secure, and entirely under your control.

Why Self-Host Nextcloud?

Before diving in, let’s address why you’d want to run your own cloud:

  • Privacy: Your files stay on your hardware, not Big Tech servers
  • No storage limits: Only limited by your disk space
  • No subscription fees: One-time setup, no monthly charges
  • Feature-rich: Calendar, contacts, notes, tasks, video calls, and hundreds of apps
  • Data portability: Easy to backup and migrate

The main trade-off? You’re responsible for maintenance and security. But with Docker, this becomes surprisingly manageable.

Prerequisites

Before starting, you’ll need:

  • A server running Linux (Ubuntu 22.04/24.04, Debian 12, or similar)
  • Docker and Docker Compose installed
  • A domain name pointing to your server (for SSL)
  • At least 2GB RAM (4GB recommended)
  • 20GB+ storage for the base install

If you haven’t installed Docker yet, here’s the quick version:

1
2
3
4
5
6
7
8
9
# Install Docker
curl -fsSL https://get.docker.com | sh

# Add your user to the docker group
sudo usermod -aG docker $USER

# Log out and back in, then verify
docker --version
docker compose version

Project Structure

I recommend organizing your Docker projects consistently. Here’s the structure we’ll use:

/opt/docker/nextcloud/
├── docker-compose.yml
├── .env
├── data/
│   └── nextcloud/
├── db/
└── redis/

Create the directory structure:

1
2
sudo mkdir -p /opt/docker/nextcloud/{data/nextcloud,db,redis}
cd /opt/docker/nextcloud

The Docker Compose File

Here’s the complete docker-compose.yml. I’ll explain each section afterward:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
version: "3.8"

services:
  nextcloud:
    image: nextcloud:29-apache
    container_name: nextcloud
    restart: unless-stopped
    depends_on:
      - db
      - redis
    environment:
      - MYSQL_HOST=db
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - REDIS_HOST=redis
      - NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_DOMAIN}
      - OVERWRITEPROTOCOL=https
      - OVERWRITECLIURL=https://${NEXTCLOUD_DOMAIN}
      - APACHE_DISABLE_REWRITE_IP=1
      - TRUSTED_PROXIES=${TRUSTED_PROXIES}
    volumes:
      - ./data/nextcloud:/var/www/html
    networks:
      - nextcloud-internal
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nextcloud.rule=Host(`${NEXTCLOUD_DOMAIN}`)"
      - "traefik.http.routers.nextcloud.entrypoints=websecure"
      - "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
      - "traefik.http.services.nextcloud.loadbalancer.server.port=80"
      - "traefik.http.middlewares.nextcloud-headers.headers.stsSeconds=31536000"
      - "traefik.http.middlewares.nextcloud-headers.headers.stsIncludeSubdomains=true"
      - "traefik.http.middlewares.nextcloud-dav.redirectregex.regex=^https://(.*)/.well-known/(card|cal)dav"
      - "traefik.http.middlewares.nextcloud-dav.redirectregex.replacement=https://$${1}/remote.php/dav/"
      - "traefik.http.middlewares.nextcloud-dav.redirectregex.permanent=true"
      - "traefik.http.routers.nextcloud.middlewares=nextcloud-headers,nextcloud-dav"

  db:
    image: mariadb:11
    container_name: nextcloud-db
    restart: unless-stopped
    command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
    volumes:
      - ./db:/var/lib/mysql
    networks:
      - nextcloud-internal

  redis:
    image: redis:7-alpine
    container_name: nextcloud-redis
    restart: unless-stopped
    volumes:
      - ./redis:/data
    networks:
      - nextcloud-internal

networks:
  nextcloud-internal:
    driver: bridge
  proxy:
    external: true

Understanding the Configuration

Nextcloud Container:

  • Uses the official Apache-based image (more straightforward than fpm variants)
  • Connects to MariaDB and Redis for database and caching
  • The OVERWRITEPROTOCOL=https is crucial for proper HTTPS behind a reverse proxy
  • TRUSTED_PROXIES tells Nextcloud to trust headers from your reverse proxy

MariaDB Container:

  • The command flags enable binary logging for better crash recovery
  • MariaDB 11 is well-tested with Nextcloud and performs excellently

Redis Container:

  • Provides memory caching for drastically improved performance
  • Alpine variant keeps the image small

Environment Variables

Create a .env file with your configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Domain Configuration
NEXTCLOUD_DOMAIN=cloud.yourdomain.com

# Database Configuration
MYSQL_ROOT_PASSWORD=super-secure-root-password-change-me
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
MYSQL_PASSWORD=another-secure-password-change-me

# Reverse Proxy Configuration
TRUSTED_PROXIES=172.16.0.0/12

Generate strong passwords:

1
2
# Generate random passwords
openssl rand -base64 32

Security note: Never commit .env files to version control. Add .env to your .gitignore.

Reverse Proxy Options

You have several options for the reverse proxy. I’ll cover Traefik (my recommendation) and Caddy.

The docker-compose above includes Traefik labels. If you don’t have Traefik running, here’s a minimal setup:

Create /opt/docker/traefik/docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
version: "3.8"

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    command:
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.email=your@email.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy

networks:
  proxy:
    external: true

Create the proxy network first:

1
docker network create proxy

Option 2: Caddy

If you prefer Caddy’s simplicity, remove the Traefik labels from the Nextcloud compose file and add:

1
2
    ports:
      - "8080:80"

Then create a Caddyfile:

cloud.yourdomain.com {
    reverse_proxy localhost:8080

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
    }

    redir /.well-known/carddav /remote.php/dav/ 301
    redir /.well-known/caldav /remote.php/dav/ 301
}

Starting the Stack

With everything configured, start the containers:

1
2
3
4
5
6
7
cd /opt/docker/nextcloud

# Start the stack
docker compose up -d

# Watch the logs
docker compose logs -f nextcloud

The first start takes a few minutes as Nextcloud initializes the database. You’ll see messages about creating tables and configuring the system.

Initial Setup

  1. Open https://cloud.yourdomain.com in your browser
  2. Create your admin account (use a strong password!)
  3. The database should auto-configure from environment variables
  4. Click “Install” and wait for completion

If you see database connection errors, wait a minute for MariaDB to fully initialize, then refresh.

Essential Post-Install Configuration

Configure Redis Caching

Edit the Nextcloud config file to enable Redis:

1
2
3
docker exec -it nextcloud bash
apt update && apt install -y nano
nano /var/www/html/config/config.php

Add these lines before the closing );:

1
2
3
4
5
6
7
  'memcache.local' => '\\OC\\Memcache\\APCu',
  'memcache.distributed' => '\\OC\\Memcache\\Redis',
  'memcache.locking' => '\\OC\\Memcache\\Redis',
  'redis' => [
    'host' => 'redis',
    'port' => 6379,
  ],

Exit the container and restart Nextcloud:

1
2
exit
docker compose restart nextcloud

Set Up Background Jobs

Nextcloud needs to run background tasks regularly. The most reliable method is a system cron job:

1
2
3
4
5
# Add cron job for Nextcloud
sudo crontab -e

# Add this line:
*/5 * * * * docker exec -u www-data nextcloud php cron.php

Then change the background jobs setting in Nextcloud:

  • Go to Settings → Administration → Basic settings
  • Under “Background jobs”, select Cron

Configure Email

For password resets and notifications, configure SMTP:

  1. Go to Settings → Administration → Basic settings
  2. Under “Email server”, enter your SMTP details

If you don’t have an SMTP server, I recommend using a free tier from:

  • Brevo (formerly Sendinblue): 300 emails/day free
  • Mailgun: 5,000 emails/month free

Enable HTTPS Strict Mode

In your config.php, ensure these settings exist:

1
2
3
  'overwriteprotocol' => 'https',
  'overwrite.cli.url' => 'https://cloud.yourdomain.com',
  'htaccess.RewriteBase' => '/',

Then regenerate the .htaccess:

1
docker exec -u www-data nextcloud php occ maintenance:update:htaccess

Performance Optimization

PHP Memory Limit

The default 512MB is usually fine, but for large file operations, increase it:

1
2
3
4
docker exec -it nextcloud bash
echo 'memory_limit = 1024M' > /usr/local/etc/php/conf.d/memory-limit.ini
exit
docker compose restart nextcloud

Enable OPcache JIT

For PHP 8+, the JIT compiler provides significant performance gains:

1
2
3
4
5
6
7
docker exec -it nextcloud bash
cat > /usr/local/etc/php/conf.d/opcache-jit.ini << 'EOF'
opcache.jit=1255
opcache.jit_buffer_size=128M
EOF
exit
docker compose restart nextcloud

Database Maintenance

Run these periodically (monthly is fine):

1
2
3
4
5
# Add missing indices
docker exec -u www-data nextcloud php occ db:add-missing-indices

# Convert filecache bigint
docker exec -u www-data nextcloud php occ db:convert-filecache-bigint

Preview Generation

File previews can be slow. Install the Preview Generator app:

  1. Go to Apps → Recommended apps
  2. Install “Preview Generator”
  3. Set up a cron job:
1
2
# Add to crontab
0 * * * * docker exec -u www-data nextcloud php occ preview:pre-generate

Security Hardening

Fail2Ban Integration

Protect against brute force attacks by integrating with Fail2Ban. First, enable Nextcloud’s logging:

In config.php:

1
2
3
  'log_type' => 'file',
  'logfile' => '/var/www/html/data/nextcloud.log',
  'loglevel' => 2,

Create /etc/fail2ban/filter.d/nextcloud.conf:

1
2
3
4
[Definition]
failregex = ^.*Login failed: '.*' \(Remote IP: '<HOST>'.*$
            ^.*"remoteAddr":"<HOST>".*"message":"Login failed:.*$
datepattern = %%Y-%%m-%%dT%%H:%%M:%%S

Create /etc/fail2ban/jail.d/nextcloud.conf:

1
2
3
4
5
6
7
[nextcloud]
enabled = true
port = http,https
filter = nextcloud
logpath = /opt/docker/nextcloud/data/nextcloud/data/nextcloud.log
maxretry = 5
bantime = 3600

Restart Fail2Ban:

1
sudo systemctl restart fail2ban

Two-Factor Authentication

Enable 2FA for all users:

  1. Go to Apps → Security
  2. Install “Two-Factor TOTP Provider”
  3. Each user can then enable 2FA in their personal settings

Essential Apps to Install

Navigate to Apps and consider installing:

  • Calendar: Full CalDAV calendar with sharing
  • Contacts: CardDAV contact management
  • Notes: Markdown note-taking
  • Tasks: Todo list synced via CalDAV
  • Talk: Video calls and chat
  • Deck: Kanban-style project management
  • Memories: Photo management (excellent Google Photos alternative)
  • News: RSS feed reader
  • Bookmarks: Bookmark manager

Mobile and Desktop Sync

Desktop Client

Download from nextcloud.com/install for:

  • Windows
  • macOS
  • Linux (AppImage, Flatpak, or distro packages)

Mobile Apps

  • Android: Nextcloud (Play Store / F-Droid)
  • iOS: Nextcloud (App Store)

For contacts and calendar sync on mobile, use:

  • Android: DAVx⁵
  • iOS: Built-in CalDAV/CardDAV support

Backup Strategy

Never skip backups. Here’s a simple script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
# /opt/docker/nextcloud/backup.sh

BACKUP_DIR="/backup/nextcloud/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"

# Enable maintenance mode
docker exec -u www-data nextcloud php occ maintenance:mode --on

# Backup database
docker exec nextcloud-db mysqldump -u nextcloud -p'YOUR_PASSWORD' nextcloud > "$BACKUP_DIR/database.sql"

# Backup data (rsync for incremental)
rsync -a /opt/docker/nextcloud/data/nextcloud/ "$BACKUP_DIR/data/"

# Backup config
cp /opt/docker/nextcloud/data/nextcloud/config/config.php "$BACKUP_DIR/"

# Disable maintenance mode
docker exec -u www-data nextcloud php occ maintenance:mode --off

# Keep only last 7 days
find /backup/nextcloud -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \;

echo "Backup completed: $BACKUP_DIR"

Add to cron for daily backups:

1
0 3 * * * /opt/docker/nextcloud/backup.sh

Troubleshooting Common Issues

“Access through untrusted domain”

Add your domain to trusted domains:

1
docker exec -u www-data nextcloud php occ config:system:set trusted_domains 0 --value="cloud.yourdomain.com"

“Your data directory is readable by other users”

Fix permissions:

1
2
3
docker exec -it nextcloud bash
chmod 750 /var/www/html/data
exit

Slow file uploads

Check your PHP upload limits and reverse proxy timeout settings. In Traefik, add:

1
- "traefik.http.middlewares.nextcloud-buffering.buffering.maxRequestBodyBytes=10737418240"

Database locked errors

Usually a sign of missing Redis locking. Verify Redis is working:

1
2
docker exec nextcloud-redis redis-cli ping
# Should return: PONG

Updating Nextcloud

Updates are straightforward with Docker:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
cd /opt/docker/nextcloud

# Pull new images
docker compose pull

# Restart with new images
docker compose up -d

# Run upgrade routine
docker exec -u www-data nextcloud php occ upgrade

# Check for issues
docker exec -u www-data nextcloud php occ maintenance:repair

Important: Always backup before updating, and check the Nextcloud changelog for breaking changes.

Conclusion

You now have a fully functional, production-ready Nextcloud instance running on Docker. With proper Redis caching, background jobs, and security hardening, it should perform excellently for personal or small team use.

The beauty of self-hosting Nextcloud is that it grows with your needs. Start with file sync, then add calendar and contacts, then explore the vast app ecosystem. Unlike cloud services, you’re never locked in — your data is always yours.

Next steps to consider:

  • Set up Collabora or OnlyOffice for document editing
  • Configure external storage (S3, SMB shares)
  • Explore the federation features for sharing across instances

If you run into issues, the Nextcloud community forums are incredibly helpful. Happy self-hosting!


Have questions about this guide? Found an error? Drop a comment below or reach out on social media.