Install Nextcloud on Ubuntu Server

Why Ubuntu for Nextcloud?

Ubuntu Server LTS is the most common Docker host for self-hosting, and Nextcloud is the most popular self-hosted cloud platform. This guide covers the Ubuntu-specific setup: installing Docker CE from the official repository (not the Snap package), configuring UFW firewall rules to coexist with Docker’s iptables, setting up unattended security updates, and tuning the system for Nextcloud’s storage-heavy workload.

For Nextcloud’s features, configuration, and troubleshooting, see the main Nextcloud guide.

Prerequisites

  • Ubuntu Server 22.04 LTS or 24.04 LTS
  • Docker and Docker Compose installed (guide)
  • 2 GB of free RAM (4 GB recommended for multiple users)
  • 10 GB of free disk space for the application, plus storage for user files
  • A domain name (recommended for HTTPS and mobile app sync)
  • Root or sudo access

Platform Setup

Install Docker CE

Do not use Ubuntu’s docker.io snap package — it has permission issues with bind mounts and is frequently out of date. Install from Docker’s official repository:

sudo apt update && sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker $USER

Log out and back in for the group change to take effect.

UFW and Docker

Docker bypasses UFW by default — it writes its own iptables rules that take priority over UFW. This means exposed container ports are reachable even if UFW blocks them. Two approaches:

Option A: Accept Docker’s behavior (simpler). Docker manages its own port access. Only use UFW for non-Docker services (SSH, etc.). This is fine for home networks.

Option B: Force Docker through UFW. Add to /etc/docker/daemon.json:

{
  "iptables": false
}

Then restart Docker (sudo systemctl restart docker) and manage all port access through UFW. Be careful — this breaks container-to-container networking unless you add explicit UFW rules.

Recommended UFW rules for Nextcloud:

sudo ufw allow 22/tcp comment "SSH"
sudo ufw allow 8080/tcp comment "Nextcloud HTTP"
sudo ufw allow 443/tcp comment "Nextcloud HTTPS (if using reverse proxy)"
sudo ufw enable

Prepare Storage

For dedicated file storage, mount a separate disk or partition:

# Example: mount a second disk for Nextcloud data
sudo mkdir -p /mnt/nextcloud-data
# Format if needed: sudo mkfs.ext4 /dev/sdb1
sudo mount /dev/sdb1 /mnt/nextcloud-data
echo "/dev/sdb1 /mnt/nextcloud-data ext4 defaults,noatime 0 2" | sudo tee -a /etc/fstab
sudo chown -R $USER:$USER /mnt/nextcloud-data

This keeps user files off the OS disk, simplifying backups and disk expansion.

Docker Compose Configuration

Create the project directory:

mkdir -p ~/nextcloud && cd ~/nextcloud

Create docker-compose.yml:

services:
  db:
    image: postgres:17-alpine
    container_name: nextcloud-db
    restart: unless-stopped
    volumes:
      - nextcloud-db:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      # CHANGE THIS — use a strong password
      POSTGRES_PASSWORD: "change-this-db-password"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U nextcloud"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: nextcloud-redis
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    image: nextcloud:33.0.0-apache
    container_name: nextcloud
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - nextcloud-html:/var/www/html
      # Optional: bind mount for data on a separate disk
      # - /mnt/nextcloud-data:/var/www/html/data
    environment:
      # Database
      POSTGRES_HOST: db
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: "change-this-db-password"
      # Cache — dramatically improves UI responsiveness
      REDIS_HOST: redis
      # Admin account (first run only)
      NEXTCLOUD_ADMIN_USER: admin
      NEXTCLOUD_ADMIN_PASSWORD: "change-this-admin-password"
      # Trusted domains — add your server IP and domain
      NEXTCLOUD_TRUSTED_DOMAINS: "localhost your-server-ip your-domain.com"
      # PHP tuning
      PHP_MEMORY_LIMIT: "512M"
      PHP_UPLOAD_LIMIT: "16G"
      APACHE_BODY_LIMIT: "0"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  cron:
    image: nextcloud:33.0.0-apache
    container_name: nextcloud-cron
    restart: unless-stopped
    volumes:
      - nextcloud-html:/var/www/html
    entrypoint: /cron.sh
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

volumes:
  nextcloud-db:
  nextcloud-html:

Start the stack:

docker compose up -d

First startup takes 1-2 minutes for database initialization.

Initial Setup

  1. Open http://your-server-ip:8080 in a browser
  2. The admin account is created automatically from the environment variables
  3. Install recommended apps when prompted (Calendar, Contacts, Talk)
  4. Go to Administration > Basic settings and confirm background jobs is set to Cron

Set Trusted Proxies (if using a reverse proxy)

docker compose exec -u www-data app php occ config:system:set overwriteprotocol --value="https"
docker compose exec -u www-data app php occ config:system:set trusted_proxies 0 --value="172.16.0.0/12"

Without overwriteprotocol, Nextcloud enters a redirect loop behind HTTPS proxies.

Ubuntu-Specific Optimization

Docker log rotation. Without this, container logs grow unbounded. Add to /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Restart Docker after editing: sudo systemctl restart docker

Unattended security updates:

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Swap space. If your server has only 2 GB of RAM, add swap as a safety net:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

inotify limits. Nextcloud’s file watcher needs higher inotify limits for large libraries:

echo "fs.inotify.max_user_watches=524288" | sudo tee -a /etc/sysctl.d/99-nextcloud.conf
sudo sysctl -p /etc/sysctl.d/99-nextcloud.conf

Reverse Proxy

Set up HTTPS with Nginx Proxy Manager or Caddy:

  1. Point your domain (e.g., cloud.example.com) at the server
  2. Create a proxy host forwarding to http://nextcloud:80 (or your-server-ip:8080)
  3. Enable SSL with Let’s Encrypt
  4. Add custom Nginx config: client_max_body_size 16G;
  5. Configure trusted proxies and overwrite protocol (see above)

See Reverse Proxy Setup for detailed instructions.

Backup

# Database dump
docker compose exec nextcloud-db pg_dump -U nextcloud nextcloud > nextcloud-db-$(date +%Y%m%d).sql

# Application data (stop briefly for consistency)
docker compose stop app cron
docker run --rm -v nextcloud-html:/data -v $(pwd):/backup alpine tar czf /backup/nextcloud-data-$(date +%Y%m%d).tar.gz /data
docker compose start app cron

Both the database and the /var/www/html volume are critical — the database has user accounts, metadata, and sharing permissions; the HTML volume has files, config, and installed apps.

See Backup Strategy for the 3-2-1 rule.

Troubleshooting

Port 8080 not reachable despite UFW being open

Docker may be routing traffic through its own iptables chain. Check with:

sudo iptables -L DOCKER -n

If Docker’s chain is empty and iptables: false is set in daemon.json, you need explicit UFW rules for Docker’s bridge network.

Slow performance with many files

  1. Confirm Redis is connected: Administration > Overview should show “Redis” under “Memcache”
  2. Increase OPcache: add PHP_OPCACHE_MEMORY_CONSUMPTION: "256" to environment
  3. Verify cron is running (not AJAX): docker compose logs cron --tail 10
  4. Use PostgreSQL, not SQLite — the environment above already uses PostgreSQL

”Access through untrusted domain”

Add the domain:

docker compose exec -u www-data app php occ config:system:set trusted_domains 2 --value="your-domain.com"

Large file uploads failing

The effective upload limit is the smallest value across all layers: PHP (PHP_UPLOAD_LIMIT), Apache (APACHE_BODY_LIMIT), reverse proxy (client_max_body_size), and any load balancer or CDN in front.

Database connection errors on first start

If the app container starts before PostgreSQL is ready, it will fail. The depends_on with condition: service_healthy in the Compose file above handles this. If you still see errors, restart:

docker compose down && docker compose up -d

Resource Requirements

  • RAM: 512 MB idle with Redis and PostgreSQL. 1-2 GB under active use.
  • CPU: Low for file storage. Medium for document editing and preview generation.
  • Disk: 1 GB for the application, plus all user file storage

Comments