Nextcloud Docker Installation: Ditch Google Drive for Your Own Cloud

Learn how to self-host your own Nextcloud instance on a VPS using Docker Compose. Complete step-by-step setup with database and reverse proxy.

System Requirements & Prerequisites

Before deploying Nextcloud, ensure your VPS meets the following hardware and software requirements:

  • Virtual Private Server (VPS): A minimum of 2 GB RAM, 2 vCPUs, and sufficient storage (SSD/NVMe recommended).
  • Operating System: Linux (Ubuntu 22.04 LTS or Debian 12 recommended).
  • DNS: An A record pointing your domain (e.g., nextcloud.example.com) to the VPS public IP address.
  • Docker: Docker Engine version 24.0.0+ and Docker Compose v2.20.0+ installed.
  • Ports: Ports 80 and 443 must be open and not bound by any other web server (such as standalone Nginx or Apache).

Directory Structure

Create the required application directories. This structure separates config files, databases, and persistent data to simplify backups and maintenance:

mkdir -p /opt/nextcloud/data/{nextcloud,db,redis,traefik}
touch /opt/nextcloud/data/traefik/acme.json
chmod 600 /opt/nextcloud/data/traefik/acme.json
cd /opt/nextcloud

The resulting directory layout is structured as follows:

/opt/nextcloud/
├── docker-compose.yml
└── data/
    ├── nextcloud/  # Nextcloud application files & user data
    ├── db/         # PostgreSQL database cluster
    ├── redis/      # Redis cache data
    └── traefik/
        └── acme.json  # Let's Encrypt certificates

Docker Compose Configuration

Create a docker-compose.yml file in /opt/nextcloud/ containing the configuration below. Replace nextcloud.example.com with your target domain, your-email@example.com with your Let's Encrypt registration email, and generate strong database passwords.

version: '3.8'

services:
  db:
    image: postgres:16-alpine
    container_name: nextcloud-db
    restart: always
    volumes:
      - ./data/db:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=nextcloud
      - POSTGRES_PASSWORD=secure_postgres_db_password_here
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U nextcloud"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: nextcloud-redis
    restart: always
    volumes:
      - ./data/redis:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

  app:
    image: nextcloud:28-apache
    container_name: nextcloud-app
    restart: always
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - ./data/nextcloud:/var/www/html
    environment:
      - POSTGRES_HOST=db
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=nextcloud
      - POSTGRES_PASSWORD=secure_postgres_db_password_here
      - REDIS_HOST=redis
      - NEXTCLOUD_TRUSTED_DOMAINS=nextcloud.example.com
      - TRUSTED_PROXIES=172.16.0.0/12 192.168.0.0/16 10.0.0.0/8 172.18.0.0/16
      - OVERWRITEPROTOCOL=https
      - OVERWRITECLIURL=https://nextcloud.example.com
      - PHP_MEMORY_LIMIT=1024M
      - PHP_UPLOAD_LIMIT=16G
      - NC_default_phone_region=US
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nextcloud.rule=Host(`nextcloud.example.com`)"
      - "traefik.http.routers.nextcloud.entrypoints=websecure"
      - "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
      - "traefik.http.services.nextcloud.loadbalancer.server.port=80"
      # CalDAV and CardDAV redirection middlewares
      - "traefik.http.routers.nextcloud.middlewares=nextcloud-dav,nextcloud-headers"
      - "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"
      # Security Headers
      - "traefik.http.middlewares.nextcloud-headers.headers.customresponseheaders.Strict-Transport-Security=max-age=15552000; includeSubDomains"

  cron:
    image: nextcloud:28-apache
    container_name: nextcloud-cron
    restart: always
    entrypoint: /cron.sh
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - ./data/nextcloud:/var/www/html

  traefik:
    image: traefik:v2.10
    container_name: traefik
    restart: always
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./data/traefik:/letsencrypt"