Nextcloud SSL Certificate: Enabling HTTPS and Fixing Mixed Content and Redirect Issues

Nextcloud running over HTTP is a serious risk for a file storage application — your login credentials, file contents, and share links all travel in plaintext. But enabling HTTPS on Nextcloud involves more than just installing a certificate on the web server. Nextcloud generates URLs internally and needs to know it is behind HTTPS, or you get mixed-content warnings, broken thumbnails, and redirect loops. This article covers enabling TLS with nginx, fixing the common Nextcloud HTTPS configuration issues, and setting up automatic certificate renewal.

What makes Nextcloud HTTPS different

Most web applications are passive about their URL — they just serve whatever the browser requests. Nextcloud is active: it generates URLs for file previews, WebDAV endpoints, share links, and federation. If Nextcloud thinks it is running on HTTP, it generates http:// URLs for all of these, which the browser blocks as mixed content when the page itself is loaded over HTTPS.

This is why configuring TLS on Nextcloud is a two-part task:

  1. Configure the web server (nginx/Apache) to serve HTTPS.
  2. Configure Nextcloud’s internal URL handling to match.

Prerequisites

  • Nextcloud 27 or later (examples use Nextcloud 28)
  • nginx + PHP-FPM (or Apache + mod_php)
  • A domain name with DNS pointing to your server
  • Let’s Encrypt or a certificate from a trusted CA

Step 1: Obtain a certificate

certbot certonly --standalone -d cloud.example.com

# If nginx is already running:
certbot certonly --webroot -w /var/www/nextcloud -d cloud.example.com

Step 2: Configure nginx for Nextcloud with TLS

The official Nextcloud nginx configuration includes TLS support. Create /etc/nginx/sites-available/nextcloud:

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name cloud.example.com;

    # Let Certbot use this for renewal
    location ^~ /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name cloud.example.com;

    ssl_certificate     /etc/letsencrypt/live/cloud.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cloud.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling        on;
    ssl_stapling_verify on;

    # Security headers
    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options SAMEORIGIN always;
    add_header X-Permitted-Cross-Domain-Policies none always;
    add_header Referrer-Policy no-referrer always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Remove X-Powered-By header
    fastcgi_hide_header X-Powered-By;

    root /var/www/nextcloud;
    index index.php index.html /index.php$request_uri;

    # Max upload size
    client_max_body_size 10G;
    client_body_timeout  300s;
    fastcgi_buffers 64 4K;

    # Gzip
    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;

    # Nextcloud-recommended nginx rules
    location = / {
        if ( $http_user_agent ~ ^DavClients ) {
            rewrite ^ /remote.php/webdav/ redirect;
        }
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Well-known redirects
    location ^~ /.well-known {
        location = /.well-known/carddav { return 301 /remote.php/dav/; }
        location = /.well-known/caldav  { return 301 /remote.php/dav/; }
        location /.well-known/acme-challenge    { try_files $uri $uri/ =404; }
        location /.well-known/pki-validation    { try_files $uri $uri/ =404; }
        return 301 /index.php$request_uri;
    }

    # Block direct access to these paths
    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\/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 unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_intercept_errors on;
        fastcgi_request_buffering off;
        fastcgi_read_timeout 300;
    }

    location ~ \.(?:css|js|svg|gif|png|jpg|ico|wasm|tflite|map|ogg|flac)$ {
        try_files $uri /index.php$request_uri;
        add_header Cache-Control "public, max-age=15778463, $asset_immutable";
        access_log off;
        location ~ \.wasm$ {
            default_type application/wasm;
        }
    }

    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;
    }
}

Enable the site:

sudo ln -s /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Step 3: Configure Nextcloud to use HTTPS internally

Open Nextcloud’s configuration file:

sudo nano /var/www/nextcloud/config/config.php

Update or add these settings:

<?php
$CONFIG = array (
  'trusted_domains' => array(
    0 => 'cloud.example.com',
  ),
  'overwrite.cli.url'   => 'https://cloud.example.com',
  'overwriteprotocol'   => 'https',
  'overwritehost'       => 'cloud.example.com',
  'force_language'      => 'en',
  'htaccess.RewriteBase' => '/',
  // If behind a reverse proxy:
  'trusted_proxies'     => array('127.0.0.1'),
);

The critical settings:

SettingPurpose
overwrite.cli.urlBase URL for CLI commands (occ, cron)
overwriteprotocolForces https in all generated URLs
trusted_domainsOnly allow requests from these hostnames
trusted_proxiesTrust forwarded headers from these IPs

After editing config.php, run:

sudo -u www-data php /var/www/nextcloud/occ maintenance:update:htaccess

Step 4: Enable HSTS in Nextcloud

Nextcloud’s security scan at https://cloud.example.com/settings/admin/overview will flag missing HSTS. The header is set in the nginx configuration above (Strict-Transport-Security). Nextcloud also sets it internally — enable it in config.php:

'hsts' => true,

Or handle it entirely via the nginx add_header directive, which is cleaner.

Step 5: Fix the Nextcloud security warnings

After enabling HTTPS, the Nextcloud admin panel may show warnings. Common ones and fixes:

“The PHP memory limit is below the recommended value” — not TLS-related, but common. Add to /etc/php/8.3/fpm/php.ini:

memory_limit = 512M

“Your web server is not properly set up to resolve .well-known/caldav” — the nginx well-known redirect above fixes this.

“Accessing site insecurely via HTTP” — Nextcloud itself still shows HTTP somewhere. Check overwriteprotocol in config.php.

“The reverse proxy header configuration is incorrect” — Add your proxy’s IP to trusted_proxies in config.php.

Run the security scan again:

sudo -u www-data php /var/www/nextcloud/occ security:scan

Step 6: Force HTTPS for WebDAV and CalDAV clients

Nextcloud clients (desktop sync app, iOS, Android) connect via WebDAV. After enabling HTTPS, update any existing clients:

  • Desktop: Go to Account Settings → Server Address and change to https://.
  • iOS/macOS Calendar: Go to System Preferences → Internet Accounts → remove and re-add the CalDAV account with HTTPS.
  • DAVx5 (Android): Re-enter the server URL with https://.

Certificate renewal automation

Certbot auto-renews via systemd timer. nginx reloads TLS certificates on nginx -s reload:

sudo nano /etc/letsencrypt/renewal-hooks/deploy/nginx-nextcloud.sh
#!/bin/bash
systemctl reload nginx
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/nginx-nextcloud.sh

Test:

sudo certbot renew --dry-run

Troubleshooting mixed content

After enabling HTTPS, if you still see browser console errors like Mixed Content: The page at 'https://...' was loaded over HTTPS, but requested an insecure resource 'http://...':

  1. Check overwriteprotocol in config.php — must be 'https'.
  2. Check HTTPS on in nginx’s fastcgi_param — must be present.
  3. Clear Nextcloud’s cache: sudo -u www-data php /var/www/nextcloud/occ maintenance:repair.
  4. Check the browser’s Network tab for the specific resource generating the HTTP URL.

Troubleshooting redirect loops

If you get an infinite redirect loop after enabling HTTPS:

  1. Check that trusted_proxies includes 127.0.0.1 or the proxy’s IP.
  2. Check that nginx passes X-Forwarded-Proto and X-Forwarded-Host headers.
  3. Ensure overwrite.cli.url starts with https://.
'trusted_proxies'   => array('127.0.0.1'),
'forwarded_for_headers' => array('HTTP_X_FORWARDED_FOR'),

Troubleshooting

ProblemCauseFix
Mixed content warningsoverwriteprotocol not setAdd 'overwriteprotocol' => 'https' to config.php
Redirect loopProxy header not trustedAdd proxy IP to trusted_proxies
WebDAV returns 301 to HTTPClient using http:// URLUpdate client to https://
untrusted_domain errorDomain not in trusted_domainsAdd the exact hostname to trusted_domains
Security warnings in admin panelHSTS missing or .well-known not configuredCheck nginx config and run occ maintenance:repair

Summary

Nextcloud HTTPS requires both a correct nginx TLS configuration and matching settings in config.php. Set overwriteprotocol = https, overwrite.cli.url to the HTTPS URL, and trusted_proxies to include the proxy’s IP. The nginx configuration must pass fastcgi_param HTTPS on to PHP-FPM. After making these changes, run occ maintenance:repair to rebuild internal URLs. Test with the Nextcloud built-in security scan at /settings/admin/overview.

Scroll to Top