Running a self-hosted cloud storage and collaboration platform gives your team full control over files, calendars, contacts, and office documents without routing sensitive data through third-party providers. Nextcloud is one of the most capable open-source platforms for this use case, but deploying it correctly in production — with automatic HTTPS, a reliable database backend, and hardened configuration — requires more than a quick docker run. This guide walks you through a complete production deployment of Nextcloud using Docker Compose, Caddy as a reverse proxy with automatic TLS, and PostgreSQL as the primary database, all on Ubuntu.
Architecture and flow overview
The stack uses three main services managed by Docker Compose:
- Nextcloud (app container) — the PHP-FPM application server, serving the web UI and sync API on an internal port
- PostgreSQL — the relational database backing all Nextcloud metadata, user accounts, and file index tables
- Caddy — the reverse proxy handling inbound HTTPS, automatic certificate provisioning via ACME/Let's Encrypt, and forwarding requests to the Nextcloud container
All three services communicate over a shared internal Docker network (nc_net). PostgreSQL and Nextcloud data are persisted on named Docker volumes. Caddy terminates TLS on ports 80 and 443 and proxies to the Nextcloud service on port 80 inside the container. No port other than 80 and 443 is exposed to the host.
Prerequisites
- Ubuntu 22.04 or 24.04 server with a public IP
- A DNS A record pointing your domain (e.g.,
cloud.example.com) to the server IP - Docker Engine and Docker Compose v2 installed (
docker compose versionshould print v2.x) - Ports 80 and 443 open in your firewall (UFW:
ufw allow 80/tcp && ufw allow 443/tcp) - A strong password manager to generate secrets for the PostgreSQL user and Nextcloud admin account
Step-by-step deployment
1. Create the project directory and environment file
mkdir -p /opt/nextcloud && cd /opt/nextcloud
touch .envEdit .env with your values — never commit this file to version control:
POSTGRES_DB=nextcloud
POSTGRES_USER=nc_user
POSTGRES_PASSWORD=ChangeMe_StrongPassword_42!
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=AnotherStrongPassword_99#
NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com
NC_DOMAIN=cloud.example.com
[email protected]2. Write the Docker Compose file
cat > /opt/nextcloud/docker-compose.yml << 'EOF'
version: "3.9"
services:
db:
image: postgres:16-alpine
restart: always
env_file: .env
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pg_data:/var/lib/postgresql/data
networks:
- nc_net
app:
image: nextcloud:28-apache
restart: always
depends_on:
- db
env_file: .env
environment:
POSTGRES_HOST: db
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USER}
NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_TRUSTED_DOMAINS}
OVERWRITEPROTOCOL: https
OVERWRITECLIURL: https://${NC_DOMAIN}
TRUSTED_PROXIES: 172.16.0.0/12
volumes:
- nc_data:/var/www/html
networks:
- nc_net
caddy:
image: caddy:2-alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- nc_net
volumes:
pg_data:
nc_data:
caddy_data:
caddy_config:
networks:
nc_net:
driver: bridge
EOF3. Write the Caddyfile
cat > /opt/nextcloud/Caddyfile << 'EOF'
{
email {$CADDY_EMAIL}
}
https://{$NC_DOMAIN} {
reverse_proxy app:80
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy no-referrer
}
encode gzip
# Nextcloud-required: allow .well-known paths
@well-known path /.well-known/*
handle @well-known {
reverse_proxy app:80
}
}
EOF4. Pull images and start the stack
cd /opt/nextcloud
docker compose pull
docker compose up -dOn first start, Nextcloud will initialize the database schema. This takes 60–120 seconds. Watch progress with:
docker compose logs -f appWait until you see Apache/2.4.x ... configured -- resuming normal operations before proceeding.
Configuration and secrets handling
Nextcloud persists its configuration in /var/www/html/config/config.php inside the nc_data volume. After first boot, several settings must be confirmed or added. This file is the single source of truth for your instance's runtime behaviour — misconfigurations here are responsible for the majority of post-deployment support issues including untrusted domain warnings, broken preview generation, and failed background jobs.
The recommended approach for managing Nextcloud configuration in a containerised deployment is to use the occ command-line tool (the Nextcloud equivalent of a management console) rather than editing config.php directly. This avoids PHP syntax errors that would immediately take the instance offline and provides structured validation for many parameter types.
# Open a shell into the app container
docker compose exec app bash
# Inside the container, run occ to check trusted domains
php occ config:system:get trusted_domains
# Set maintenance window and default phone region
php occ config:system:set maintenance_window_start --value=1
php occ config:system:set default_phone_region --value=US
# Exit the container
exitFor the .env file, restrict its permissions so only root can read it:
chmod 600 /opt/nextcloud/.env
chown root:root /opt/nextcloud/.envNever hard-code credentials in docker-compose.yml. Always use the env_file directive combined with environment variable references as shown above.
Verification
After the stack is running, run through this checklist:
# 1. Check all containers are running
docker compose ps
# 2. Confirm HTTPS redirects from HTTP
curl -I http://cloud.example.com
# 3. Check TLS certificate is valid
curl -sv https://cloud.example.com/ 2>&1 | grep -E "SSL|subject|expire"
# 4. Run Nextcloud's built-in system checks
docker compose exec app php occ status
docker compose exec app php occ checkNavigate to https://cloud.example.com/settings/admin/overview in a browser after logging in as admin. Resolve any warnings shown in the Security and setup warnings section. Common items to fix immediately after first boot include: enabling a memory cache (APCu or Redis), confirming the background jobs mode is set to Cron rather than AJAX, and setting the default phone region. These warnings do not prevent operation but affect performance and security compliance scores in audits.
To confirm the Caddy proxy headers are arriving correctly at the Nextcloud PHP layer, run:
docker compose exec app php occ config:system:get overwrite.cli.url
docker compose exec app php occ config:system:get overwriteprotocolBoth should return the values you set via the environment variables. If overwriteprotocol is missing or empty, Nextcloud may generate broken HTTP links in file share emails despite HTTPS working in the browser — set it explicitly with occ config:system:set overwriteprotocol --value=https.
Common issues and fixes
Nextcloud shows a blank page or 500 error after startup
The database initialization may still be running. Check docker compose logs app — if you see PDO connection failed, wait 30 more seconds and refresh. If the error persists, verify the POSTGRES_* variables in .env match exactly between the db and app services. A common mistake is leaving a trailing space in one of the password values in the .env file — PostgreSQL accepts it as part of the password while the Nextcloud connection string trims it, causing an authentication mismatch that looks like a connectivity error in the logs.
Trusted domain mismatch warning
If Nextcloud shows "Access through untrusted domain", the NEXTCLOUD_TRUSTED_DOMAINS variable was not set before first boot. Fix it at runtime:
docker compose exec app php occ config:system:set trusted_domains 1 --value=cloud.example.comFiles not syncing — Caddy returns 413 or large upload fails
Caddy does not have a default upload size limit, but the Nextcloud Apache container does. Increase it by setting a custom PHP config:
docker compose exec app bash -c "echo 'upload_max_filesize=10G' >> /usr/local/etc/php/conf.d/nextcloud.ini"
docker compose exec app bash -c "echo 'post_max_size=10G' >> /usr/local/etc/php/conf.d/nextcloud.ini"
docker compose restart appBackground jobs not running
Nextcloud requires regular cron jobs. On the host, add a crontab entry:
(crontab -l 2>/dev/null; echo "*/5 * * * * docker exec nextcloud_app_1 php -f /var/www/html/cron.php") | crontab -FAQ
Can I migrate from SQLite to PostgreSQL after the initial setup?
Yes, but it requires using the occ db:convert-type command inside the container. Back up your data volume first, then run php occ db:convert-type --all-apps pgsql nc_user 127.0.0.1 nextcloud. Plan for 15–60 minutes downtime depending on data size. It is far easier to deploy with PostgreSQL from the start as shown in this guide.
How do I upgrade Nextcloud to a new major version?
Update the image tag in docker-compose.yml (e.g., from nextcloud:28-apache to nextcloud:29-apache), pull the new image, and restart the app container. Nextcloud will run the upgrade wizard automatically on the next request. Always update one major version at a time and back up both the nc_data volume and PostgreSQL before upgrading.
How do I enable SMTP email notifications?
Add the following to the app service environment in docker-compose.yml and restart:
MAIL_FROM_ADDRESS=noreply
MAIL_DOMAIN=example.com
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=tls
[email protected]
SMTP_PASSWORD=smtppasswordWhat is the best way to back up Nextcloud data?
Use a two-part backup strategy: dump PostgreSQL with pg_dump and archive the nc_data Docker volume. Enable maintenance mode before the backup to avoid inconsistencies: docker compose exec app php occ maintenance:mode --on. Consider pairing this with the Kopia + S3 approach described in our Kopia production backup guide.
Is the Nextcloud container compatible with Podman or Kubernetes?
Yes. The official Nextcloud image runs rootless under Podman without changes to the compose file. For Kubernetes, use the official Nextcloud Helm chart, which supports the same PostgreSQL and persistent volume configuration. Both environments benefit from the same Caddy or ingress-based HTTPS setup described here.
How do I add object storage (S3) for file data?
Configure the primary storage backend in Nextcloud's config.php using the objectstore block pointing to an S3-compatible endpoint (MinIO, AWS S3, Backblaze B2). This offloads large binary files from the local volume. Refer to the official Nextcloud documentation for the full objectstore configuration keys and note that primary storage migration from local to S3 requires a one-time data migration step.
Internal links
- Production Guide: Deploy Vaultwarden with Docker Compose + Caddy on Ubuntu
- Production Guide: Deploy Authentik with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
- Production Backups with Kopia, S3 Storage, Docker Compose, and Caddy
Talk to us
If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.