How to: Configure Nextcloud with nginx Reverse Proxy on Ubuntu 24.04

Setting up Nextcloud with nginx and Docker Compose

This guide covers deploying Nextcloud using Docker Compose with an nginx reverse proxy for SSL/TLS termination. The configuration separates concerns by having nginx handle external HTTPS traffic while Nextcloud runs in isolated Docker containers.

Replace nextcloud.yourdomain.com with your actual subdomain.

Required Tools

  • Ubuntu 24.04 LTS Server
  • Docker 27.x docker --version
  • Docker Compose 2.x docker compose version
  • nginx 1.24+ nginx -v
  • certbot 2.11.0+ certbot --version
  • Cloudflare DNS (or alternative DNS provider)

Architecture Overview

The deployment consists of four main services:

  • nginx: Reverse proxy handling SSL/TLS termination on port 443
  • Nextcloud (app): PHP-FPM application container
  • PostgreSQL (db): Database backend
  • Redis: Caching layer for improved performance

Directory Structure

Following Linux Filesystem Hierarchy Standard (FHS) best practices, the deployment uses the following locations:

  • /opt/nextcloud/ - Docker Compose configuration and application files
  • /etc/nginx/sites-available/ - nginx server configuration
  • /etc/letsencrypt/ - SSL/TLS certificates managed by certbot
  • /var/backups/nextcloud/ - Database and data backups
  • /etc/systemd/system/ - systemd service files for container management

DNS Configuration

Create an A record pointing nextcloud.yourdomain.com to your server's IP address.

If using Cloudflare, ensure SSL/TLS encryption mode is set to "Full (strict)" to properly validate the certificate chain between Cloudflare and your origin server.

Obtaining SSL/TLS Certificates

Install certbot if not already present.

sudo snap install --classic certbot sudo ln -s /snap/bin/certbot /usr/bin/certbot

Request a certificate for your Nextcloud subdomain.

sudo certbot certonly --standalone -d nextcloud.yourdomain.com

The SSL certificates will be stored in /etc/letsencrypt/live/nextcloud.yourdomain.com/

Set up automatic renewal with a systemd timer.

sudo systemctl enable --now certbot.timer

Installing Docker and Docker Compose

Update the package index and install required dependencies.

sudo apt-get update sudo apt-get install ca-certificates curl

Add Docker's official GPG key.

sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc

Add the Docker repository to apt sources.

echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Install Docker Engine and Docker Compose.

sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Add your user to the docker group to run Docker commands without sudo.

sudo usermod -aG docker $USER

Log out and back in for group changes to take effect.

Creating the Docker Compose Configuration

Create a directory structure for the Nextcloud deployment. Following best practices, Docker Compose projects should reside in /opt/ for production deployments.

sudo mkdir -p /opt/nextcloud sudo chown $USER:$USER /opt/nextcloud cd /opt/nextcloud

Create a database environment file db.env to store PostgreSQL credentials.

POSTGRES_DB=nextcloud POSTGRES_USER=nextcloud POSTGRES_PASSWORD=your_secure_database_password_here

Ensure the file has restricted permissions to prevent unauthorized access.

chmod 600 db.env

Create the Docker Compose configuration file compose.yaml

volumes: db: nextcloud: services: db: image: postgres:17.6-alpine3.22 restart: always volumes: - db:/var/lib/postgresql/data env_file: - db.env redis: image: redis:alpine restart: always app: image: nextcloud:fpm-alpine restart: always volumes: - nextcloud:/var/www/html environment: - POSTGRES_HOST=db - REDIS_HOST=redis - PHP_MEMORY_LIMIT=-1 - PHP_UPLOAD_LIMIT=32G env_file: - db.env depends_on: - db - redis web: build: ./web restart: always ports: - 127.0.0.1:4430:443 volumes: - nextcloud:/var/www/html:ro - /etc/letsencrypt:/etc/letsencrypt:ro - /etc/ssl:/etc/ssl:ro depends_on: - app cron: image: nextcloud:fpm-alpine restart: always volumes: - nextcloud:/var/www/html entrypoint: /cron.sh depends_on: - db - redis

Creating the nginx Container

Create a directory for the nginx web service.

mkdir -p web

Create a Dockerfile in the web/ directory.

FROM nginx:alpine COPY nginx.conf /etc/nginx/nginx.conf

Create the nginx configuration file web/nginx.conf

worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; keepalive_timeout 65; upstream php-handler { server app:9000; } server { listen 443 ssl http2; ssl_certificate /etc/letsencrypt/live/nextcloud.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/nextcloud.yourdomain.com/privkey.pem; # Security headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Referrer-Policy "no-referrer" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; # Allow large file uploads client_max_body_size 32G; client_body_timeout 300s; fastcgi_buffers 64 4K; gzip on; gzip_vary on; gzip_comp_level 4; gzip_min_length 256; gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; root /var/www/html; location = /robots.txt { allow all; log_not_found off; access_log off; } location ^~ /.well-known { location = /.well-known/carddav { return 301 /remote.php/dav/; } location = /.well-known/caldav { return 301 /remote.php/dav/; } location ^~ /.well-known { return 301 /index.php$uri; } try_files $uri $uri/ =404; } location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; } location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } location ~ \.php(?:$|/) { rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri; fastcgi_split_path_info ^(.+?\.php)(/.*)$; set $path_info $fastcgi_path_info; try_files $fastcgi_script_name =404; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $path_info; fastcgi_param HTTPS on; fastcgi_param modHeadersAvailable true; fastcgi_param front_controller_active true; fastcgi_pass php-handler; fastcgi_intercept_errors on; fastcgi_request_buffering off; fastcgi_max_temp_file_size 0; } location ~ \.(?:css|js|svg|gif|png|jpg|ico|wasm|tflite|map|ogg|flac)$ { try_files $uri /index.php$request_uri; expires 6M; access_log off; } location ~ \.woff2?$ { try_files $uri /index.php$request_uri; expires 7d; access_log off; } location /remote { return 301 /remote.php$request_uri; } location / { try_files $uri $uri/ /index.php$request_uri; } } }

Ensure the certificate paths in web/nginx.conf match your domain.

Deploying the Containers

Build and start all services.

docker compose up -d

Check the status of all containers.

docker compose ps

All containers should show as "Up" and healthy.

View container logs if troubleshooting is needed.

docker compose logs -f app

Managing Nextcloud with systemd

For production deployments, create a systemd service to manage the Nextcloud containers. This ensures Nextcloud starts automatically on system boot and can be managed with standard systemd commands.

Create the systemd service file /etc/systemd/system/nextcloud.service

[Unit] Description=Nextcloud Docker Compose Service Requires=docker.service After=docker.service network-online.target Wants=network-online.target [Service] Type=oneshot RemainAfterExit=yes WorkingDirectory=/opt/nextcloud ExecStart=/usr/bin/docker compose up -d ExecStop=/usr/bin/docker compose down TimeoutStartSec=0 [Install] WantedBy=multi-user.target

Reload systemd to recognize the new service.

sudo systemctl daemon-reload

Enable the service to start on boot.

sudo systemctl enable nextcloud.service

The Nextcloud service can now be managed with standard systemd commands.

sudo systemctl start nextcloud sudo systemctl stop nextcloud sudo systemctl restart nextcloud sudo systemctl status nextcloud

Viewing Logs

Container logs are accessible through Docker Compose or systemd journal.

View Nextcloud application logs.

cd /opt/nextcloud docker compose logs -f app

View all container logs.

docker compose logs -f

View systemd service logs.

sudo journalctl -u nextcloud.service -f

Configuring the External nginx Reverse Proxy

The Nextcloud containers are now running and accessible on 127.0.0.1:4430. Configure the system's nginx to proxy external HTTPS requests to the Nextcloud container.

Create an nginx server configuration file /etc/nginx/sites-available/nextcloud.yourdomain.com

# nextcloud.yourdomain.com - Nextcloud File Storage & Collaboration # HTTP to HTTPS redirect server { listen 80; listen [::]:80; server_name nextcloud.yourdomain.com; return 301 https://$host$request_uri; } # HTTPS server server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name nextcloud.yourdomain.com; # SSL certificates ssl_certificate /etc/letsencrypt/live/nextcloud.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/nextcloud.yourdomain.com/privkey.pem; # SSL configuration ssl_session_timeout 5m; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; # Security headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Allow large file uploads client_max_body_size 0; # Proxy to Nextcloud container location / { proxy_set_header Host $host; proxy_pass_request_headers on; proxy_buffers 64 4k; proxy_pass https://127.0.0.1:4430; } }

Enable the site by creating a symbolic link.

sudo ln -s /etc/nginx/sites-available/nextcloud.yourdomain.com /etc/nginx/sites-enabled/

Verify nginx configuration file permissions are secure.

sudo chmod 644 /etc/nginx/sites-available/nextcloud.yourdomain.com

Test the nginx configuration for syntax errors.

sudo nginx -t

Reload nginx to apply the changes.

sudo systemctl reload nginx

Securing File Permissions

Ensure proper ownership and permissions for the Nextcloud deployment directory.

sudo chown -R $USER:$USER /opt/nextcloud sudo chmod 755 /opt/nextcloud sudo chmod 600 /opt/nextcloud/db.env

Completing Nextcloud Installation

Navigate to https://nextcloud.yourdomain.com in a web browser.

Create an administrator account by providing a username and password.

Configure the database connection:

  • Database type: PostgreSQL
  • Database user: nextcloud
  • Database password: (the password from db.env)
  • Database name: nextcloud
  • Database host: db:5432

Click "Install" to complete the setup.

Post-Installation Configuration

Navigate to the Nextcloud deployment directory.

cd /opt/nextcloud

Access the Nextcloud container shell.

docker compose exec -u www-data app php occ

Configure trusted domains to include your domain name.

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

Enable Redis caching for improved performance.

docker compose exec -u www-data app php occ config:system:set redis host --value=redis docker compose exec -u www-data app php occ config:system:set redis port --value=6379 docker compose exec -u www-data app php occ config:system:set memcache.locking --value='\OC\Memcache\Redis'

Configure background jobs to use cron.

docker compose exec -u www-data app php occ background:cron

Maintaining Nextcloud

Navigate to the Nextcloud directory for maintenance operations.

cd /opt/nextcloud

Update Nextcloud containers by pulling the latest images.

docker compose pull docker compose up -d

Alternatively, use the systemd service.

sudo systemctl restart nextcloud

Backing Up Nextcloud

Create a backup directory to store database dumps.

sudo mkdir -p /var/backups/nextcloud

Back up the PostgreSQL database.

docker compose exec -T db pg_dump -U nextcloud nextcloud | sudo tee /var/backups/nextcloud/nextcloud_db_$(date +%Y%m%d_%H%M%S).sql > /dev/null

Back up the Nextcloud data volume. The data is stored in a Docker volume which can be inspected with the following command.

docker volume inspect nextcloud_nextcloud

Create a compressed archive of the Nextcloud volume data.

sudo docker run --rm -v nextcloud_nextcloud:/data -v /var/backups/nextcloud:/backup alpine tar czf /backup/nextcloud_data_$(date +%Y%m%d_%H%M%S).tar.gz -C /data .

Automated backups can be configured using a systemd timer or cron job.

The Nextcloud installation is now complete and accessible at https://nextcloud.yourdomain.com with proper SSL/TLS encryption, security headers, and optimized caching.