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:
- Configure the web server (nginx/Apache) to serve HTTPS.
- 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:
| Setting | Purpose |
|---|---|
overwrite.cli.url | Base URL for CLI commands (occ, cron) |
overwriteprotocol | Forces https in all generated URLs |
trusted_domains | Only allow requests from these hostnames |
trusted_proxies | Trust 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://...':
- Check
overwriteprotocolinconfig.php— must be'https'. - Check
HTTPS onin nginx’sfastcgi_param— must be present. - Clear Nextcloud’s cache:
sudo -u www-data php /var/www/nextcloud/occ maintenance:repair. - 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:
- Check that
trusted_proxiesincludes127.0.0.1or the proxy’s IP. - Check that nginx passes
X-Forwarded-ProtoandX-Forwarded-Hostheaders. - Ensure
overwrite.cli.urlstarts withhttps://.
'trusted_proxies' => array('127.0.0.1'),
'forwarded_for_headers' => array('HTTP_X_FORWARDED_FOR'),
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Mixed content warnings | overwriteprotocol not set | Add 'overwriteprotocol' => 'https' to config.php |
| Redirect loop | Proxy header not trusted | Add proxy IP to trusted_proxies |
| WebDAV returns 301 to HTTP | Client using http:// URL | Update client to https:// |
untrusted_domain error | Domain not in trusted_domains | Add the exact hostname to trusted_domains |
| Security warnings in admin panel | HSTS missing or .well-known not configured | Check 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.