Nextcloud can be deployed on a VPS with Docker Compose by separating the application, PostgreSQL, Redis, and cron background tasks into individual containers. This approach simplifies service updates, infrastructure migration, and recovery after a failure.
This guide will configure:
- A Nextcloud container;
- A separate PostgreSQL database;
- Redis for caching and file locking;
- A separate cron container for background tasks;
- Persistent Docker volumes for configuration, user files, and the database;
- Nginx as a reverse proxy;
- A domain and a Let’s Encrypt HTTPS certificate;
- An increased file upload limit;
- Backup and full recovery.
External access will be provided at an address such as: https://cloud.example.com
The Nextcloud container will be accessible only locally on the VPS, while Nginx will handle incoming HTTPS requests. PostgreSQL and Redis will not be exposed to the internet and will remain inside the Docker network.
After the setup is complete, you will have a ready-to-use cloud storage service with persistent data, automatic background tasks, and a straightforward update workflow:
docker compose pull
docker compose up -d
Before updating or migrating, you need to back up three groups of data:
- A PostgreSQL dump;
- The Nextcloud configuration;
- User files.
This set of data is what will allow you to fully restore Nextcloud on a new VPS or after recreating the containers.
How Nextcloud Works with Docker Compose
Docker Compose lets you define all Nextcloud components in a single YAML file and run them as a single project. The application, database, Redis, and background tasks run in separate containers but communicate through an internal Docker network.
This separation simplifies system maintenance. The Nextcloud container can be recreated or updated independently of PostgreSQL, while persistent data is stored in Docker volumes. The official Nextcloud Docker image supports connecting to PostgreSQL and Redis through environment variables.
Required components
The configuration will include the following services:
- Nextcloud — a web application for file storage, synchronization, and collaboration;
- PostgreSQL — a separate database for users, settings, metadata, and file information;
- Redis — a cache and transactional file locking mechanism;
- cron — a separate container that regularly runs Nextcloud background tasks;
- Nginx — a reverse proxy on the host system;
- Certbot — issuance and automatic renewal of an SSL certificate;
- Docker Volumes — persistent storage for the database, configuration, and user files.
Redis reduces the load on the database and is used for file locking during concurrent operations. Nextcloud recommends Redis as a distributed cache and lock store.
The cron container will use the same image and the same volumes as the main Nextcloud container. It runs cron.php, which handles temporary file maintenance, change checks, and other application tasks. The recommended interval for running background jobs is every five minutes.
Nextcloud, PostgreSQL, Redis, Nginx, and HTTPS Architecture
The resulting architecture will look like this:
Browser
│
│ HTTPS
▼
Nginx on the VPS
│
│ HTTP via 127.0.0.1
▼
Nextcloud
├── PostgreSQL
├── Redis
├── Docker volume for the application
└── Docker volume for user files
cron container
│
└── uses the same Nextcloud data
External requests reach Nginx on ports 80 and 443. After terminating the TLS connection, Nginx forwards requests to the Nextcloud container through a local port on the VPS.
PostgreSQL and Redis are not exposed on the host system. Their names within the Compose project are used as network addresses:
db:5432
redis:6379
All containers connect to a shared internal Docker network. Only Nginx and SSH are directly accessible from the internet.
When running behind a reverse proxy, Nextcloud must be configured with the trusted proxy address and the external protocol, HTTPS. This protects against spoofed client headers and allows the application to generate links correctly.
Preparing the VPS
For a test deployment, you will need a VPS with Ubuntu, a public IPv4 address, and SSH access.
Recommended minimum configuration:
Operating system: Ubuntu 24.04 LTS
Processor: 2 vCPU
RAM: 4 GB
Disk: 30–40 GB
For Nextcloud, it is better to allocate more disk space than you would for a typical web application: it will store user files, the database, backups, and Docker images.
On a production server, disk capacity should be chosen with sufficient headroom and monitored separately from Nextcloud user quotas.
Creating a virtual machine

Create a new virtual machine in the cloud control panel. You can use nextcloud-guide as the name.
Select Ubuntu 24.04 LTS and a configuration with 2 vCPUs and 4 GB of RAM. In most cases, 30–40 GB is sufficient for the system disk.
Connect the existing network, subnet, and security group. In the inbound traffic rules, allow:
TCP 22 — SSH
TCP 80 — HTTP
TCP 443 — HTTPS
You do not need to open PostgreSQL port 5432, Redis port 6379, or the internal Nextcloud port.
When creating the VM, select an existing SSH key pair or create a new one. The private key must be stored only on the administrator’s computer.
Attaching a public IP address
After starting the VM, attach a Floating IP or another public IPv4 address to it.
The control panel should show:
- Name: nextcloud-guide;
- Status: Active;
- Private address within the cloud network;
- Attached public IP address;
- Selected VM configuration.
The public IP address is required for SSH access and for the DNS record of the Nextcloud subdomain. PostgreSQL and Redis will continue to run only within the Docker network and will not use this address.
Connecting via SSH and Updating Ubuntu
On Windows, open PowerShell or Command Prompt and navigate to the directory containing the private key: cd C:\Users\Username\Downloads
If the public IP address was previously used by another virtual machine, remove the old SSH fingerprint: ssh-keygen -R PUBLIC_IP
Connect to the server: ssh -i .\nextcloud-guide.pem ubuntu@PUBLIC_IP
Replace the file name and PUBLIC_IP with your own values.
The first time you connect, confirm the fingerprint: yes
Update the package index and upgrade the system:
sudo apt update
sudo apt upgrade -y
If the regional Ubuntu mirror temporarily returns 503 Service Unavailable errors, retry the command later or switch the package source to another official mirror.
Install the basic components: sudo apt install -y ca-certificates curl gnupg unzip ufw
After the update is complete, the VPS is ready for Docker Engine installation and preparation of the Compose project.
Installing Docker and Preparing the Project

Installing Docker Engine and the Compose Plugin
For deployment, use Docker Engine from Docker’s official repository and the Compose Plugin. In current versions, Compose runs as a subcommand: docker compose
First, remove any packages that may conflict with the official Docker version:
sudo apt remove -y \
docker.io \
docker-compose \
docker-compose-v2 \
podman-docker \
containerd \
runc
If some of these packages are not installed, apt will simply say so.
Add Docker’s official key and repository: 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
echo \
"deb [arch=$(dpkg –print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Update the package index and install Docker Engine together with the Compose Plugin:
sudo apt update
sudo apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
This exact set of packages is used in Docker’s official instructions for Ubuntu.
Enable Docker to start automatically: sudo systemctl enable –now docker
Add the current user to the docker group: sudo usermod -aG docker "$USER"
To apply the new permissions, end the SSH session: exit
Reconnect to the server: ssh -i .\nextcloud-guide.pem ubuntu@PUBLIC_IP
Verify the installation:
docker –version
docker compose version
systemctl is-active docker
systemctl is-enabled docker
Expected result:
Docker version …
Docker Compose version …
active
enabled
Additionally, run a test container: docker run –rm hello-world
After a successful check, you can remove the test image: docker image rm hello-world
Creating the project directory and environment variable file
Create a separate directory for the Compose project:
sudo mkdir -p /opt/nextcloud
sudo chown -R "$USER":"$USER" /opt/nextcloud
cd /opt/nextcloud
The main project files will be:
/opt/nextcloud/
├── compose.yaml
└── .env
compose.yaml will contain the container and volume definitions, while .env will store the database and Redis passwords, as well as other environment settings.
Generate two random passwords:
openssl rand -base64 32
openssl rand -base64 32
Create the .env file: nano .env
Add:
POSTGRES_DB=nextcloud
POSTGRES_USER=nextcloud
POSTGRES_PASSWORD=REPLACE_WITH_DATABASE_PASSWORD
REDIS_PASSWORD=REPLACE_WITH_REDIS_PASSWORD
NEXTCLOUD_DOMAIN=cloud.example.com
Replace:
- REPLACE_WITH_DATABASE_PASSWORD — with the first generated password;
- REPLACE_WITH_REDIS_PASSWORD — with the second password;
- cloud.example.com — with the future Nextcloud domain.
Save the file with Ctrl+O, confirm the filename by pressing Enter, and close the editor with Ctrl+X.
Restrict access to the file: chmod 600 .env
Check the permissions without displaying the contents: ls -l .env
The output should start with: -rw——-
Beginners should also be aware that the .env file must not be published to a Git repository. The official Nextcloud image supports the POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, REDIS_HOST, and REDIS_HOST_PASSWORD variables. For a stricter approach to storing secrets, you can also use variable variants with the _FILE suffix and Docker Secrets.
Preparing Persistent Volumes
Containers are considered ephemeral: after they are re-created, their internal filesystem layer may be removed. Therefore, the database and Nextcloud data must be stored in named Docker volumes.
The project will use three volumes:
nextcloud_postgres
nextcloud_app
nextcloud_data
Their purposes are:
| Volume | Path inside the container | Contents |
| nextcloud_postgres | /var/lib/postgresql/data | PostgreSQL database |
| nextcloud_postgres | /var/www/html | Nextcloud application, configuration, and additional apps |
| nextcloud_data | /var/www/html/data | user files |
The primary volume at /var/www/html is required for correct updates of a containerized Nextcloud installation. If necessary, the user files directory can be mounted as a separate volume to simplify backups and data migration.
You do not need to create the volumes manually. They will be declared in compose.yaml, and Docker Compose will create them automatically when the project is started for the first time.
Creating the Docker Compose configuration
Create the configuration file:
cd /opt/nextcloud
nano compose.yaml
Add the following configuration:
services:
db:
image: postgres:17-alpine
container_name: nextcloud-db
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
– nextcloud_postgres:/var/lib/postgresql/data
healthcheck:
test:
– CMD-SHELL
– pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}
interval: 10s
timeout: 5s
retries: 10
redis:
image: redis:alpine
container_name: nextcloud-redis
restart: unless-stopped
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
command:
– sh
– -c
– exec redis-server –requirepass "$$REDIS_PASSWORD"
healthcheck:
test:
– CMD-SHELL
– redis-cli -a "$$REDIS_PASSWORD" ping | grep PONG
interval: 10s
timeout: 5s
retries: 10
app:
image: nextcloud:apache
container_name: nextcloud-app
restart: unless-stopped
ports:
– "127.0.0.1:8080:80"
environment:
POSTGRES_HOST: db
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
REDIS_HOST: redis
REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_DOMAIN}
volumes:
– nextcloud_app:/var/www/html
– nextcloud_data:/var/www/html/data
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
cron:
image: nextcloud:apache
container_name: nextcloud-cron
restart: unless-stopped
entrypoint: /cron.sh
environment:
POSTGRES_HOST: db
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
REDIS_HOST: redis
REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
volumes:
– nextcloud_app:/var/www/html
– nextcloud_data:/var/www/html/data
depends_on:
app:
condition: service_started
volumes:
nextcloud_postgres:
name: nextcloud_postgres
nextcloud_app:
name: nextcloud_app
nextcloud_data:
name: nextcloud_data
Save the file and close the editor.
Configuring PostgreSQL
The db service runs PostgreSQL in a separate container:
db:
image: postgres:17-alpine
Credentials are provided via the .env file:
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
Database data is stored in a volume:
volumes:
– nextcloud_postgres:/var/lib/postgresql/data
Therefore, recreating the nextcloud-db container will not delete the database. Port 5432 is not published on the VPS: only services within the Compose project can connect to PostgreSQL.
The health check uses the pg_isready command:
healthcheck:
test:
– CMD-SHELL
– pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}
The Nextcloud container will start only after PostgreSQL becomes healthy.
Configuring Redis
The redis service is used for caching and transactional file locking:
redis:
image: redis:alpine
Redis is protected with a password from .env:
command:
– sh
– -c
– exec redis-server –requirepass "$$REDIS_PASSWORD"
The double dollar sign in $$REDIS_PASSWORD is required so that the variable is expanded by the shell inside the container, rather than by Docker Compose when it reads the YAML file.
The health check sends Redis the PING command and expects a PONG response.
By default, Nextcloud can store locks in the database, but this creates additional load. Redis is well suited for transactional locking because it keeps lock values for as long as the application needs them.
Port 6379 is also not published on the VPS.
Configuring the Nextcloud Container
The main application runs as the app service:
app:
image: nextcloud:apache
The apache variant includes a web server inside the container. On the host system, it is bound only to the local interface:
ports:
– "127.0.0.1:8080:80"
Port 8080 is not accessible from the Internet. Later, Nginx will receive requests over HTTPS and forward them to: http://127.0.0.1:8080
The connection to PostgreSQL is made using the service name: POSTGRES_HOST: db
Redis is available at: REDIS_HOST: redis
Docker Compose automatically creates an internal network for the project, so the containers do not need fixed IP addresses.
The parameter: NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_DOMAIN}
adds the domain that will be used later to Nextcloud’s list of trusted domains. The official image supports automatic configuration of the database and trusted domains through environment variables.
Two volumes are mounted for the application:
volumes:
– nextcloud_app:/var/www/html
– nextcloud_data:/var/www/html/data
The second mount overlays the data directory inside the first volume, keeping user files in separate storage.
Configuring a dedicated cron container
Background tasks are run by a separate service:
cron:
image: nextcloud:apache
entrypoint: /cron.sh
It uses the same image and mounts the same volumes as the main container:
volumes:
– nextcloud_app:/var/www/html
– nextcloud_data:/var/www/html/data
This gives the cron container access to the installed Nextcloud instance, its configuration, additional apps, and user data.
The /cron.sh script periodically runs cron.php. The separate container continues to run background jobs independently of user activity in the web interface. The official Nextcloud image repository includes sample Compose configurations with a dedicated cron service.
The cron container does not publish any ports and does not accept external requests.
Validating the Compose Configuration

Before starting, check the syntax:
cd /opt/nextcloud
docker compose config –quiet
If the command completes with no output, the YAML file is valid.
To view the resulting configuration, run: docker compose config
To safely check the list of services, run: docker compose config –services
Expected output:
db
redis
app
cron
Check the declared volumes: docker compose config –volumes
Expected output:
nextcloud_app
nextcloud_data
nextcloud_postgres
Next, proceed to start Nextcloud.
Starting Nextcloud
Building and starting the containers
Because the project uses prebuilt PostgreSQL, Redis, and Nextcloud images, a local build with docker compose build is not required. Docker Compose will pull the images from the registry and create the containers automatically.
Before the first run, add the external Nextcloud address settings to the app service:
environment:
POSTGRES_HOST: db
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
REDIS_HOST: redis
REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_DOMAIN}
OVERWRITEHOST: ${NEXTCLOUD_DOMAIN}
OVERWRITEPROTOCOL: https
OVERWRITECLIURL: https://${NEXTCLOUD_DOMAIN}
These settings are required so that Nextcloud generates HTTPS links correctly when running behind a reverse proxy. The official Docker image supports the OVERWRITEHOST, OVERWRITEPROTOCOL, and OVERWRITECLIURL variables; the corresponding values are written to the application configuration during installation.
Validate the modified file:
cd /opt/nextcloud
docker compose config –quiet
Pull the required images: docker compose pull
The first download may take several minutes. Then start the project: docker compose up -d
The -d option starts the containers in detached mode.
Docker Compose automatically:
- Creates the project’s internal network;
- Creates the defined volumes;
- Starts PostgreSQL and Redis;
- Waits until the health check passes;
- Starts Nextcloud and the cron container.
The official Nextcloud image is designed to run via Compose with a separate database and persistent volumes. The Apache variant includes a built-in web server and can run behind an external reverse proxy.
Checking the status of services

Check the status of the Compose project: docker compose ps
After the initial startup completes, four containers should be running:
nextcloud-app
nextcloud-cron
nextcloud-db
nextcloud-redis
PostgreSQL and Redis should show the healthy status, while Nextcloud and cron should show Up.
For a concise status check, run:
docker compose ps –format \
"table {{.Name}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"
Example output:
| NAME | IMAGE | STATUS | PORTS |
| nextcloud-app | nextcloud:apache | Up | 127.0.0.1:8080->80/tcp |
| nextcloud-cron | nextcloud:apache | Up | |
| nextcloud-db | postgres:17-alpine | Up (healthy) | 5432/tcp |
| nextcloud-redis | redis:alpine | Up (healthy) | 6379/tcp |
PostgreSQL and Redis may show the containers’ internal ports, but they are not published on the VPS public interface.
Check the local Nextcloud response: curl -I http://127.0.0.1:8080
Before installation is complete, the server may return a redirect to the initial setup page: HTTP/1.1 302 Found
This is expected: the Nextcloud web server is already running and is waiting for an administrator account to be created.
If one of the containers is restarting or has exited with an error, check the logs: docker compose logs –tail=100
Checking Persistent Volumes

List the volumes associated with the project:
docker volume ls \
–filter name=nextcloud
Expected result:
| DRIVER | VOLUME NAME |
| local | nextcloud_app |
| local | nextcloud_data |
| local | nextcloud_postgres |
Check the mount points:
docker inspect nextcloud-app \
–format ‘{{range .Mounts}}{{println .Name "->" .Destination}}{{end}}’
Expected output:
nextcloud_app -> /var/www/html
nextcloud_data -> /var/www/html/data
For PostgreSQL:
docker inspect nextcloud-db \
–format ‘{{range .Mounts}}{{println .Name "->" .Destination}}{{end}}’
Result: nextcloud_postgres -> /var/lib/postgresql/data
Persistent volumes separate data from the container lifecycle. Containers can be recreated after image updates without deleting the database, configuration, or user files. However, the docker compose down -v command removes volumes and must not be used on an operational server.
Connecting a Domain and HTTPS
Nextcloud is already available locally at 127.0.0.1:8080, but this port is not exposed to the Internet. External connections will be handled by Nginx using a Let’s Encrypt HTTPS certificate.
Creating a DNS Record
Open the DNS management panel for the domain you are using and create an A record.
Example:
| Parameter | Value |
| Type | A |
| Name | wordpress |
| IPv4 address | 203.0.113.10 |
| TTL | Auto or the default value |
As a result, the subdomain cloud.example.com should point to the public IPv4 address of the VPS.
When using Cloudflare, it is convenient to leave the record in DNS only mode during the initial certificate issuance.
This lets you verify a direct connection to the VPS without an additional proxy. After setup is complete, you can enable Cloudflare proxying separately, after first checking HTTPS and your plan’s request size limits.
Check DNS from your local computer: nslookup cloud.example.com
Or on the VPS: getent hosts cloud.example.com
The command should return the public IP address of the virtual machine you created.
Do not proceed with certificate issuance until the domain resolves to the correct address.
Installing and Configuring Nginx
Install Nginx:
sudo apt update
sudo apt install -y nginx
Enable it to start automatically: sudo systemctl enable –now nginx
Check the status:
systemctl is-active nginx
systemctl is-enabled nginx
Expected result:
active
enabled
If UFW is used, allow SSH and web traffic:
sudo ufw allow OpenSSH
sudo ufw allow ‘Nginx Full’
sudo ufw enable
Check the rules: sudo ufw status
You do not need to open ports 8080, 5432, or 6379.
Configuring a reverse proxy for Nextcloud
Create a virtual host configuration: sudo nano /etc/nginx/sites-available/nextcloud
Add the following:
server {
listen 80;
listen [::]:80;
server_name cloud.example.com;
client_max_body_size 10G;
proxy_request_buffering off;
location = /.well-known/carddav {
return 301 $scheme://$host/remote.php/dav;
}
location = /.well-known/caldav {
return 301 $scheme://$host/remote.php/dav;
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
proxy_connect_timeout 60;
proxy_buffering off;
}
}
Replace cloud.example.com with your own subdomain.
The proxy_pass http://127.0.0.1:8080 directive forwards requests to the Nextcloud container.
The Host, X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto headers allow the application to determine the external domain, protocol, and client address. Nextcloud provides separate documentation for configuring trusted reverse proxies and parameters for enforcing the external protocol.
Parameter: proxy_request_buffering off;
prevents Nginx from reading the entire request body before passing it to the application. This is useful for uploading large files. The client_max_body_size directive sets the maximum size of the client request body.
Create a symbolic link:
sudo ln -s \
/etc/nginx/sites-available/nextcloud \
/etc/nginx/sites-enabled/nextcloud
Remove the default configuration:
sudo rm -f /etc/nginx/sites-enabled/default
Check the syntax: sudo nginx -t
Expected output:
syntax is ok
test is successful
Apply the configuration: sudo systemctl reload nginx
The Nextcloud page should now be accessible over HTTP: http://cloud.example.com
Do not create an administrator account or enter passwords over an unencrypted connection until the certificate has been issued.
Issuing an SSL Certificate
Install Certbot and the Nginx plugin: sudo apt install -y certbot python3-certbot-nginx
Request the certificate: sudo certbot –nginx -d cloud.example.com
Certbot will prompt for:
- Email address;
- Acceptance of the terms of service;
- Consent to or refusal of the mailing list.
After the domain is successfully validated, Certbot will obtain a Let’s Encrypt certificate and update the Nginx configuration for HTTPS. The Nginx plugin can automatically install the issued certificate and include files from the /etc/letsencrypt/live/ directory.
Check the configuration:
sudo nginx -t
sudo systemctl reload nginx
Check the certificate: sudo certbot certificates
Check automatic renewal: systemctl status certbot.timer –no-pager
Then run a test renewal: sudo certbot renew –dry-run
Open in a browser: https://cloud.example.com
The initial Nextcloud setup page should appear, and the browser should show a secure HTTPS connection.
Additionally, check the HTTP redirect: curl -I http://cloud.example.com
A redirect to HTTPS is expected:
HTTP/1.1 301 Moved Permanently
Location: https://cloud.example.com/
After the certificate has been issued, check the HTTPS headers: curl -I https://cloud.example.com
A 200 response, a 302 response, or another normal Nextcloud redirect means that Nginx and the application container are connected correctly.
Initial Nextcloud Setup
After enabling HTTPS, open the following URL in your browser: https://cloud.example.com
The initial Nextcloud setup wizard will appear. At this stage, create a local administrator account and verify the PostgreSQL connection settings.
Creating an administrator account
In the administrator creation section, specify:
Username: nextcloud-admin
Password: a strong, unique password
Do not use the PostgreSQL, Redis, or VPS account password for the administrator account. All passwords must be different.
The administrator username can be any name you choose. For a production server, it is better to avoid obvious options such as admin or administrator.
Save the password in a password manager.
Connecting to PostgreSQL
Expand the storage and database configuration section if it does not open automatically. Select: PostgreSQL
Specify the parameters from the .env file:
- Database user: nextcloud
- Database password: the POSTGRES_PASSWORD value
- Database name: nextcloud
- Database host: db
In the host field, use the exact Compose service name: db
Do not specify localhost or the VPS public IP address. PostgreSQL runs in a separate container, and the name db is resolved through the internal Docker Compose network.
If the parameters were passed through the main container’s environment variables, some fields may be filled in automatically. The official Nextcloud Docker image supports automatic PostgreSQL configuration using POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD.
Leave the data directory path unchanged: /var/www/html/data
This directory is mounted to a separate persistent volume, nextcloud_data.
Click the button to complete the installation: Install
Database initialization and table creation may take several minutes. Do not refresh the page or restart the containers until the process is complete.
If the browser returns an error or the installation hangs, check the application logs:
cd /opt/nextcloud
docker compose logs –tail=100 app
Check PostgreSQL: docker compose logs –tail=100 db
The database status should remain healthy: docker compose ps db
Verifying login to the dashboard

After installation, Nextcloud will either sign you in automatically with the account you created or display the login page.
On first login, a welcome dialog may appear prompting you to install client applications. You can close it.
Make sure the main sections are available:
- Files
- Photos
- Activity
- Contacts
- Calendar
The actual set of menu items depends on the apps enabled in the installed Nextcloud version.
Upload a small test file, such as a text document: nextcloud-test.txt
After the upload, refresh the page and make sure the file is still in the list.
You can check whether the file exists in the volume from the application container:
docker exec -u www-data nextcloud-app \
php occ files:scan –all
This command updates the file index for all users. It is not required for normal uploads through the web interface: Nextcloud automatically adds such files to the index.
Check the installation status:
docker exec -u www-data nextcloud-app \
php occ status
Example of expected output:
installed: true
maintenance: false
needsDbUpgrade: false
The version shown in the output depends on the Docker image being used.
To check the database, run:
docker exec nextcloud-db \
pg_isready -U nextcloud -d nextcloud
Expected result: /var/run/postgresql:5432 – accepting connections
Configuring Redis and Background Tasks
Redis was started together with the Compose project, and its address and password were passed to the Nextcloud container through environment variables. After installation, verify that the application is actually using Redis for caching and transactional file locking.
You should also switch background jobs from AJAX to Cron. In production deployments, Nextcloud recommends the system Cron mode because AJAX runs tasks only when users visit pages and is considered the least reliable option.
Connecting Redis for caching and file locking
The official Docker image uses the following variables:
REDIS_HOST
REDIS_HOST_PORT
REDIS_HOST_PASSWORD
to connect Nextcloud to a separate Redis server. In our configuration, the Redis address is the Compose service name: redis
The official Nextcloud documentation recommends Redis as a distributed cache and as a store for transactional locks. Using the database for locks adds extra load to PostgreSQL.
First, check that Redis is reachable from within its container:
cd /opt/nextcloud
docker compose exec redis sh -c \
‘redis-cli -a "$REDIS_PASSWORD" ping’
Expected response: PONG
Check the container status: docker compose ps redis
It should have the status: Up (healthy)
Checking the Redis configuration
View the combined Nextcloud system configuration:
docker exec -u www-data nextcloud-app \
php occ config:list system
The full output may include internal parameters. Review it carefully before publishing it.
To safely check individual values, use:
docker exec -u www-data nextcloud-app \
php occ config:system:get redis host
Expected result: redis
Check the port:
docker exec -u www-data nextcloud-app \
php occ config:system:get redis port
Expected result: 6379
Check the distributed cache:
docker exec -u www-data nextcloud-app \
php occ config:system:get memcache.distributed
And transactional file locking:
docker exec -u www-data nextcloud-app \
php occ config:system:get memcache.locking
Expected value for both parameters: \OC\Memcache\Redis
Do not print the Redis password parameter: the command will display the secret in plain text.
For a compact, safe check, run:
echo "Redis container:" && \
docker compose ps redis && \
echo && \
echo "Redis host:" && \
docker exec -u www-data nextcloud-app \
php occ config:system:get redis host && \
echo "Distributed cache:" && \
docker exec -u www-data nextcloud-app \
php occ config:system:get memcache.distributed && \
echo "File locking:" && \
docker exec -u www-data nextcloud-app \
php occ config:system:get memcache.locking
If the memcache.distributed and memcache.locking parameters are missing, add them with occ:
docker exec -u www-data nextcloud-app \
php occ config:system:set memcache.distributed \
–value=’\OC\Memcache\Redis’
docker exec -u www-data nextcloud-app \
php occ config:system:set memcache.locking \
–value=’\OC\Memcache\Redis’
Then run the check again. Nextcloud requires not only an accessible Redis server, but also the corresponding redis block in the application’s system configuration.
Switching background jobs to Cron

By default, a new installation may use AJAX mode. In this mode, a separate background job is triggered when a user opens a page. For a permanent server, it is better to use Cron, which runs jobs independently of site traffic.
Our Compose project already runs a separate container: nextcloud-cron
It uses the official /cron.sh script and the same volumes as the main Nextcloud container. The official image repository provides examples of running a separate cron service in a Compose configuration.
Make sure the container is running: docker compose ps cron
Check its latest logs: docker compose logs –tail=50 cron
Switch Nextcloud to Cron mode manually:
docker exec -u www-data nextcloud-app \
php occ background:cron
Expected result: Set mode for background jobs to ‘cron’
The background:cron command is the standard way to select Cron mode via the occ interface. Nextcloud recommends it for production installations.
Check the saved mode:
docker exec -u www-data nextcloud-app \
php occ config:app:get core backgroundjobs_mode
Expected result: cron
Now open the user menu in Nextcloud and go to: Administration settings → Basic settings
Find the section: Background jobs
The selected option should be: Cron
After cron.php runs for the first time, Nextcloud may also switch the mode automatically.
Wait at least five minutes, refresh the settings page, and make sure a recent time for the last background job run is displayed there.
Increasing the file upload limit
The size of an uploaded file is limited at several levels:
- PHP settings inside the Nextcloud container;
- The built-in Apache server;
- The external Nginx reverse proxy;
- Available disk space on the VPS;
- Any limitations imposed by the CDN or proxy service.
If you change only one setting, uploading a large file may still fail at another level.
For example, in this guide, we will set the maximum size of a single request to 10 GB.
Changing PHP settings
The official Nextcloud Docker image supports this variable: PHP_UPLOAD_LIMIT
It sets the values of the PHP parameters upload_max_filesize and post_max_size. By default, the image uses a 512M limit.
Open the Compose configuration:
cd /opt/nextcloud
nano compose.yaml
Add the following to the environment block of the app service:
app:
image: nextcloud:apache
container_name: nextcloud-app
restart: unless-stopped
ports:
– "127.0.0.1:8080:80"
environment:
POSTGRES_HOST: db
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
REDIS_HOST: redis
REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_DOMAIN}
OVERWRITEHOST: ${NEXTCLOUD_DOMAIN}
OVERWRITEPROTOCOL: https
OVERWRITECLIURL: https://${NEXTCLOUD_DOMAIN}
PHP_UPLOAD_LIMIT: 10G
APACHE_BODY_LIMIT: 10737418240
The PHP_UPLOAD_LIMIT=10G variable changes the PHP limits.
The APACHE_BODY_LIMIT=10737418240 value allows Apache to accept a request body of up to 10 GB. It is specified in bytes. The default Apache limit in the official Nextcloud image is 1 GiB, so it must also be increased for large uploads.
You do not need to add these parameters to the cron service: the background jobs container does not handle user uploads.
Validate the Compose file: docker compose config –quiet
If the command completes without output, the syntax is correct.
Changing the Nginx limit
Open the reverse proxy configuration: sudo nano /etc/nginx/sites-available/nextcloud
Make sure the following directive is present inside the server block: client_max_body_size 10G;
The configuration should include:
server {
server_name cloud.example.com;
client_max_body_size 10G;
proxy_request_buffering off;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
proxy_buffering off;
}
}
The proxy_request_buffering off directive passes the request body to the application as it is received, instead of buffering the entire request on the Nginx side first.
Increased timeouts help prevent the connection from being interrupted when transferring large files over a slow connection.
Check the configuration: sudo nginx -t
Expected result:
syntax is ok
test is successful
Apply the changes: sudo systemctl reload nginx
Restarting services and checking the limit

Recreate the application container to apply the new environment variables:
cd /opt/nextcloud
docker compose up -d –force-recreate app
Check the status: docker compose ps app
The container should have the status Up.
Make sure PHP has picked up the new values:
docker compose exec app php -r \
‘echo "upload_max_filesize: " . ini_get("upload_max_filesize") . PHP_EOL; echo "post_max_size: " . ini_get("post_max_size") . PHP_EOL;’
Expected result:
upload_max_filesize: 10G
post_max_size: 10G
Check the Apache limit: docker compose exec app printenv APACHE_BODY_LIMIT
Expected value: 10737418240
Then open: Administration settings → System
The increased maximum upload size should be displayed in the PHP settings section.
Keep in mind that the maximum request size does not guarantee a successful upload if there is not enough free space on the system disk. Before transferring large files, check the available space: df -h
When using Cloudflare or another external proxy, the actual limit may also depend on the restrictions of the selected service.
Updating Nextcloud
A containerized Nextcloud installation should be updated by updating the Docker image. The built-in web updater is intended primarily for standard installations and should not replace container updates. For Docker, the instructions for the image you are using are considered the most accurate.
Before updating, you must:
- Check whether a new version is available;
- Review the release notes;
- Verify app compatibility;
- Create a full backup;
- Do not skip multiple major versions in a single upgrade.
For a production server, it is better to pin the image to a major version instead of using a floating tag: image: nextcloud:MAJOR-apache
The same tag must be used by the app and cron services. After verifying the new major version, update the tag manually and perform the upgrade step by step.
Preparing a Backup Before an Upgrade
Before any upgrade, save the following:
- A PostgreSQL dump;
- The Docker volume with the Nextcloud code and configuration;
- The Docker volume with user files;
- The compose.yaml and .env files;
- The Nginx configuration.
Create the directory:
sudo mkdir -p /opt/backups/nextcloud
sudo chown "$USER":"$USER" /opt/backups/nextcloud
Create a PostgreSQL backup:
cd /opt/nextcloud
docker compose exec -T db \
pg_dump \
-U "$POSTGRES_USER" \
-d "$POSTGRES_DB" \
-Fc \
> /opt/backups/nextcloud/postgresql.dump
Variables from .env are not always loaded into the current shell automatically. Therefore, you can export them before running the command:
set -a
source .env
set +a
Create archives of the application and user files:
docker run –rm \
-v nextcloud_app:/source:ro \
-v /opt/backups/nextcloud:/backup \
alpine \
tar -czf /backup/nextcloud-app.tar.gz -C /source .
docker run –rm \
-v nextcloud_data:/source:ro \
-v /opt/backups/nextcloud:/backup \
alpine \
tar -czf /backup/nextcloud-data.tar.gz -C /source .
Save the configuration files:
cp compose.yaml /opt/backups/nextcloud/
cp .env /opt/backups/nextcloud/
sudo cp /etc/nginx/sites-available/nextcloud \
/opt/backups/nextcloud/nginx-nextcloud.conf
Restrict access to the .env backup: chmod 600 /opt/backups/nextcloud/.env
Verify the created files: ls -lh /opt/backups/nextcloud
Proceed with the upgrade only after the database dump and both archives have been created.
Putting Nextcloud into maintenance mode
Maintenance mode terminates active user sessions and blocks new connections for the duration of the update. Nextcloud recommends using it for operations that require temporarily disabling access to the instance.
Enable maintenance mode:
docker compose exec -u www-data app \
php occ maintenance:mode –on
Check its status:
docker compose exec -u www-data app \
php occ maintenance:mode
Expected result: Maintenance mode is currently enabled
Stop the background jobs container: docker compose stop cron
This prevents background operations from running while the database and apps are being updated.
Pulling a new Docker image
Check the current Nextcloud version:
docker compose exec -u www-data app \
php occ status
You can also display the image currently in use:
docker inspect nextcloud-app \
–format ‘{{.Config.Image}}’
If the major version is pinned in compose.yaml, update the tag for both the app and cron services at the same time.
Example:
services:
app:
image: nextcloud:NEW_MAJOR-apache
cron:
image: nextcloud:NEW_MAJOR-apache
Do not skip multiple major versions at once. First update the instance to the next supported major version, complete the migrations, and only then repeat the process.
Validate the configuration: docker compose config –quiet
Pull the new images: docker compose pull app cron
The command downloads the updated layers but does not yet replace the running application container.
Recreating containers
Recreate the main container: docker compose up -d –no-deps app
Verify that it is running: docker compose ps app
View the log: docker compose logs –tail=100 app
When the new official image starts, its entrypoint synchronizes the Nextcloud files in the persistent /var/www/html directory. This mechanism works when using the standard Apache or PHP-FPM startup command.
Do not delete the nextcloud_app volume before upgrading. It contains the configuration, installed apps, and instance state.
Running the built-in upgrade
After starting the new container, check the status:
docker compose exec -u www-data app \
php occ status
If Nextcloud indicates that the database or apps need to be upgraded, run:
docker compose exec -u www-data app \
php occ upgrade
The occ upgrade command migrates the database and upgrades the apps, but it does not replace the Docker image or the application files.
Do not close the SSH session until the operation is complete. On large instances, the upgrade may take a considerable amount of time.
After the migration, it is recommended to run the repair commands:
docker compose exec -u www-data app \
php occ maintenance:repair
docker compose exec -u www-data app \
php occ db:add-missing-indices
docker compose exec -u www-data app \
php occ db:add-missing-columns
docker compose exec -u www-data app \
php occ db:add-missing-primary-keys
Nextcloud intentionally excludes some long-running migrations from the main upgrade process. You can run them separately after the upgrade is complete.
Checking the version and disabling maintenance mode
Check the instance status:
docker compose exec -u www-data app \
php occ status
The output should include the following values:
installed: true
maintenance: true
needsDbUpgrade: false
Disable maintenance mode:
docker compose exec -u www-data app \
php occ maintenance:mode –off
Start the updated cron container: docker compose up -d cron
Check all services: docker compose ps
Make sure that:
- The application is running;
- PostgreSQL and Redis have the status healthy;
- The cron container is running;
- NeedsDbUpgrade is set to false;
- The web interface is accessible over HTTPS.
Check the logs: docker compose logs –tail=50 app cron
Then open: https://cloud.example.com
Sign in and go to: Administration settings → Overview
The page should not display a warning about an incomplete database upgrade.
After successful verification, you can remove old unused images: docker image prune
Do not use the -a option until you are sure that you will no longer need to roll back to the previous image.
Backing Up Nextcloud
Without a backup, a containerized installation remains vulnerable: disk failure, a failed update, an administrator error, or database corruption can result in the loss of files and settings. For Nextcloud, it is important to back up not only user documents but also the PostgreSQL database, the application configuration, and persistent Docker volumes.
Data to back up
A complete Nextcloud backup typically includes four groups of data:
- The PostgreSQL database;
- The Nextcloud configuration and code in a persistent volume;
- User files;
- Project support files, including compose.yaml, .env, and the Nginx configuration.
If you save only the files but do not create a database dump, users, applications, settings, access permissions, and file information may be missing after recovery. Conversely, if you save only the database but lose the user data, you will not be able to restore the cloud to a working state.
For example, create a backup directory:
sudo mkdir -p /opt/backups/nextcloud
sudo chown "$USER":"$USER" /opt/backups/nextcloud
Before creating a backup, it is useful to put Nextcloud into maintenance mode to prevent file changes during the backup:
cd /opt/nextcloud
docker compose exec -u www-data app php occ maintenance:mode –on
After the backup is complete, maintenance mode can be disabled.
Creating a PostgreSQL Dump
The easiest way to back up the database is with pg_dump. If the environment variables are stored in .env, you can first load them into the current session:
cd /opt/nextcloud
set -a
source .env
set +a
Then create a PostgreSQL dump:
docker compose exec -T db \
pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc \
> /opt/backups/nextcloud/postgresql.dump
The -Fc format creates a compressed PostgreSQL dump that is convenient to restore later with pg_restore.
Verify that the file was created: ls -lh /opt/backups/nextcloud/postgresql.dump
If the file was not created or has a size of zero, do not continue the backup process until the issue has been resolved.
Archiving configuration and user files

Next, save the data from the persistent volumes. A convenient way to do this is to run a temporary alpine container that archives the contents of the volume to the backup directory.
Archive the configuration and application:
docker run –rm \
-v nextcloud_app:/source:ro \
-v /opt/backups/nextcloud:/backup \
alpine \
tar -czf /backup/nextcloud-app.tar.gz -C /source .
Archive the user files:
docker run –rm \
-v nextcloud_data:/source:ro \
-v /opt/backups/nextcloud:/backup \
alpine \
tar -czf /backup/nextcloud-data.tar.gz -C /source .
If the PostgreSQL database also uses a separate Docker volume and you want an additional file-level copy, you can save that as well:
docker run –rm \
-v nextcloud_db:/source:ro \
-v /opt/backups/nextcloud:/backup \
alpine \
tar -czf /backup/nextcloud-db-volume.tar.gz -C /source .
Also save the project’s supporting files:
cp /opt/nextcloud/compose.yaml /opt/backups/nextcloud/
cp /opt/nextcloud/.env /opt/backups/nextcloud/
sudo cp /etc/nginx/sites-available/nextcloud /opt/backups/nextcloud/nginx-nextcloud.conf
Because .env may contain passwords and other sensitive data, restrict its access permissions:
chmod 600 /opt/backups/nextcloud/.env
After that, check the contents of the backup directory:
ls -lh /opt/backups/nextcloud
When the backup is complete, disable maintenance mode:
cd /opt/nextcloud
docker compose exec -u www-data app php occ maintenance:mode –off
Proceed to the backup.
Copying a backup from the VPS
Storing a backup only on the same VPS is not enough. If the disk fails, the virtual machine is deleted, or the server is compromised, the local backup may be lost along with the primary data.
At a minimum, download the backup directory to your local computer using scp:
scp -i .\nextcloud-guide.pem -r \
ubuntu@YOUR_SERVER_IP:/opt/backups/nextcloud \
Replace YOUR_SERVER_IP with the public IP address of your VM.
If backups need to be stored on a regular basis, it is better to use separate storage: object storage, a backup server, or an external cloud. For a practical guide, it is enough to demonstrate the principle: the backup must exist outside the VPS itself.
Full Restore from a Backup
A full restore lets you redeploy Nextcloud from scratch after a failure, a failed upgrade, or a migration to a new server. The restore workflow is the reverse of the backup process: first, prepare a clean environment; then restore the volumes and the database; finally, start the application and verify that it is working.
Preparing a clean environment
To perform the recovery, you will need a fresh or wiped VPS with the following in place:
- Docker Engine;
- Docker Compose Plugin;
- Nginx;
- domain and HTTPS configuration;
- a Nextcloud project directory.
First, create the project working directory:
sudo mkdir -p /opt/nextcloud
sudo chown "$USER":"$USER" /opt/nextcloud
cd /opt/nextcloud
Restore the supporting files from the backup to this directory:
cp /opt/backups/nextcloud/compose.yaml /opt/nextcloud/
cp /opt/backups/nextcloud/.env /opt/nextcloud/
If you use a separate Nginx configuration, restore it as well:
sudo cp /opt/backups/nextcloud/nginx-nextcloud.conf /etc/nginx/sites-available/nextcloud
sudo ln -sf /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/nextcloud
sudo nginx -t && sudo systemctl reload nginx
Then create empty Docker volumes and start the containers once so the environment can initialize:
cd /opt/nextcloud
docker compose up -d
After the first run, you can stop the containers to safely replace their contents with the backup: docker compose down
Restoring Docker Volumes and Nextcloud Files
If the volumes already exist, remove them so the restore is performed to clean storage: docker volume rm nextcloud_app nextcloud_data nextcloud_db
Then create them again:
docker volume create nextcloud_app
docker volume create nextcloud_data
docker volume create nextcloud_db
Extract the application archive:
docker run –rm \
-v nextcloud_app:/target \
-v /opt/backups/nextcloud:/backup \
alpine \
sh -c "cd /target && tar -xzf /backup/nextcloud-app.tar.gz"
Extract the user files:
docker run –rm \
-v nextcloud_data:/target \
-v /opt/backups/nextcloud:/backup \
alpine \
sh -c "cd /target && tar -xzf /backup/nextcloud-data.tar.gz"
If you also saved a file-level copy of the PostgreSQL volume, you can restore it in the same way. However, in most cases, a clean PostgreSQL container and restoration from a database dump are sufficient.
Restoring the PostgreSQL Database
Start only the database service:
cd /opt/nextcloud
docker compose up -d db
Wait until PostgreSQL is ready: docker compose ps db
If the database already contains internal tables, you can recreate it from scratch. To do this, first drop the old database and create a new one:
set -a
source .env
set +a
docker compose exec -T db psql -U "$POSTGRES_USER" -d postgres \
-c "DROP DATABASE IF EXISTS \"$POSTGRES_DB\";"
docker compose exec -T db psql -U "$POSTGRES_USER" -d postgres \
-c "CREATE DATABASE \"$POSTGRES_DB\" OWNER \"$POSTGRES_USER\";"
Now restore the dump:
docker compose exec -T db pg_restore \
-U "$POSTGRES_USER" \
-d "$POSTGRES_DB" \
–clean –if-exists \
< /opt/backups/nextcloud/postgresql.dump
After the restore is complete, you can verify that the database is accessible: docker compose exec -T db psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "\dt"
If a list of tables is displayed, the database structure has been restored.
Checking access permissions
After restoring the files, make sure that the Nextcloud container can read and modify them. Typically, directories inside the container should be owned by the web server user.
Start the application container: docker compose up -d app redis cron
Then correct the file ownership inside the container: docker compose exec app chown -R www-data:www-data /var/www/html
If necessary, you can also check the permissions: docker compose exec app find /var/www/html -maxdepth 2 -type d | head
In most cases, this is enough for Nextcloud to correctly detect the configuration, apps, and user files.
Starting the containers and disabling maintenance mode

After the files and database have been restored, make sure all services are running: docker compose ps
Check the Nextcloud status: docker compose exec -u www-data app php occ status
If the instance is still in maintenance mode, disable it: docker compose exec -u www-data app php occ maintenance:mode –off
Also make sure background jobs are running in cron mode: docker compose exec -u www-data app php occ background:cron
Then open the site using its domain name and log in to the web interface. Check that:
- the Nextcloud home page opens;
- uploaded files are available;
- administrator login works;
- new files are saved;
- there are no errors in the Administration settings section.
If the web interface opens over HTTPS, user data is visible, and background jobs and the database are running without errors, the restoration can be considered successful.
Check After a VPS Reboot
After completing the installation, configuring background jobs, and restoring from a backup, you should verify that the entire infrastructure starts automatically. This check confirms that Nextcloud will return to an operational state after a planned restart, a kernel update, or an emergency VPS reboot.
Checking Docker, Nginx, and containers

Before rebooting, make sure the automatic restart policy is set for every service in compose.yaml: restart: unless-stopped
This policy must be present for the following containers:
nextcloud-app
nextcloud-cron
nextcloud-db
nextcloud-redis
Check the current Compose configuration:
cd /opt/nextcloud
docker compose config –quiet
Then reboot the VPS: sudo reboot
The SSH connection will be closed automatically. Wait about a minute, then reconnect to the server: ssh -i .\nextcloud-guide.pem ubuntu@PUBLIC_IP
Check the status of Docker and Nginx:
printf "Docker: "
systemctl is-active docker
printf "Nginx: "
systemctl is-active nginx
Both services should return: active
Make sure they are enabled to start automatically:
printf "Docker autostart: "
systemctl is-enabled docker
printf "Nginx autostart: "
systemctl is-enabled nginx
Expected result: enabled
Check the Compose project containers:
cd /opt/nextcloud
docker compose ps
For more compact output, use:
docker compose ps –format \
"table {{.Name}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"
The list should include four running containers:
nextcloud-app
nextcloud-cron
nextcloud-db
nextcloud-redis
PostgreSQL and Redis should reach the healthy state. Immediately after a reboot, this may take a few seconds.
You can check everything with a single command:
printf "Docker: "
systemctl is-active docker
printf "Nginx: "
systemctl is-active nginx
echo
docker compose ps –format \
"table {{.Name}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"
Also check the status of the application itself: docker compose exec -u www-data app php occ status
In normal operation, the output contains:
installed: true
maintenance: false
needsDbUpgrade: false
Check Redis availability:
docker compose exec redis sh -c \
‘redis-cli -a "$REDIS_PASSWORD" ping’
Expected response: PONG
Check the PostgreSQL connection:
set -a
source .env
set +a
docker compose exec db \
pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
Then open the domain in a browser: https://cloud.example.com
Sign in and make sure that:
- The Nextcloud interface opens over HTTPS;
- Previously uploaded files are available;
- New files can be uploaded;
- Maintenance mode is disabled;
- Background jobs use Cron;
- There is no warning in the administration area that the database needs to be upgraded.
After the check completes successfully, the installation is ready for use.
Conclusion

In this guide, Nextcloud was deployed on a VPS using Docker Compose. The application, PostgreSQL, Redis, and background jobs run in separate containers, which simplifies infrastructure maintenance and updates.
Persistent Docker volumes store the database, configuration, and user files independently of the container lifecycle. PostgreSQL is used instead of the built-in SQLite database, while Redis handles caching and transactional file locking. A separate cron container runs background jobs without depending on user activity.
External access is provided through Nginx and a Let’s Encrypt HTTPS certificate. The application container is exposed only on the VPS’s local interface, while PostgreSQL and Redis remain inside the Docker network and do not expose their ports to the internet.
The following were also configured:
- An increased file upload limit;
- Safe updating of the Nextcloud Docker image;
- Backups of PostgreSQL, configuration, and user data;
- Full instance recovery;
- Automatic service startup after a VPS reboot.
For ongoing operation, it is important to create backups regularly and store them outside the primary server. Before upgrading, you should check app compatibility and avoid skipping multiple major Nextcloud versions at once.
You should also monitor:
- Available disk space;
- HTTPS certificate expiration;
- The status of PostgreSQL and Redis;
- Execution of background jobs;
- Container logs;
- Availability of backups for recovery.
The result is a self-hosted cloud storage instance with PostgreSQL, Redis, HTTPS, persistent data, and a reproducible recovery procedure.
FAQ
Why Nextcloud Needs Redis
Redis is used for distributed caching and transactional file locking. Without it, Nextcloud can store locks in the database, which adds extra load to PostgreSQL during file uploads, moves, and concurrent edits.
For file locking, Nextcloud recommends Redis because it stores lock data more predictably than Memcached. The configuration typically includes the following parameters:
‘memcache.distributed’ => ‘\OC\Memcache\Redis’,
‘memcache.locking’ => ‘\OC\Memcache\Redis’,
Redis does not replace PostgreSQL and does not store Nextcloud’s primary data. It complements the database and helps improve application performance.
Why PostgreSQL Is Preferable to SQLite
SQLite is suitable for a small test deployment or a single-user instance with minimal load. It stores the database in a single local file, which limits concurrent query processing.
PostgreSQL runs as a separate database server and is better suited for a long-running Nextcloud deployment with multiple users, background jobs, and regular file synchronization. It is also easier to back up, migrate, and restore the database separately using pg_dump and pg_restore.
The official Nextcloud Docker image can use SQLite by default, but the installation wizard supports connecting to an existing PostgreSQL database.
Can external storage be used for user files?
This option may be more cost-effective and convenient than expanding the storage of our VPS. Nextcloud supports various options for storing user data, such as object storage (S3), which has a lower cost in the cloud. A change to config.php will be required, according to the documentation.
Where Nextcloud user files are stored
In the configuration used in this guide, data is split across three persistent Docker volumes:
nextcloud_app
nextcloud_data
nextcloud_postgres
User files are stored in: nextcloud_data → /var/www/html/data
Configuration, installed apps, and other instance files are stored in: nextcloud_app → /var/www/html
The PostgreSQL database is stored in: nextcloud_postgres → /var/lib/postgresql/data
The exact location of the volume on the host system is managed by Docker. It is safer to work with the data through containers, by archiving volumes, and using standard backup commands rather than manually editing the contents of the Docker directory.
Can Nextcloud be updated through the web interface?
A built-in web updater is available, but installations using Docker should follow the update procedure for the specific Docker image. First, the new image is pulled, then the containers are recreated and the required migrations are run with occ upgrade.
The Nextcloud documentation specifically states that for Docker, Snap, prebuilt VMs, and package-based installations, you must follow the instructions for the corresponding installation method. The built-in updater also does not create a backup of the database or the data directory.
For Docker Compose, the basic sequence is as follows:
docker compose pull app cron
docker compose up -d –no-deps app
docker compose exec -u www-data app php occ upgrade
docker compose up -d cron
The occ upgrade command performs database and app migrations, but it does not download or replace the Nextcloud files itself.
What must be included in a backup
For a full recovery, include:
- PostgreSQL dump;
- Nextcloud configuration and files;
- The user data directory;
- Additional apps and themes;
- Compose.yaml and .env;
- Nginx configuration;
- SSL configuration and other service files, if required.
The database and user files must correspond to the same point in time. Therefore, before copying, it is recommended to enable maintenance mode and temporarily stop background jobs.
The .env file contains passwords, so it must be stored in a secure location with restricted access permissions. The backup should be stored not only on the source VPS, but also in external storage.
How Often Cron Background Jobs Should Run
Nextcloud recommends running cron.php every five minutes. Background jobs clean up temporary data, process queues, maintain applications, and perform other operations that should not depend on users visiting the site.
In a containerized configuration, a separate cron service uses the official /cron.sh script. You can enable cron mode for background jobs with the following command: docker compose exec -u www-data app php occ background:cron
You can check the configured mode under: Administration settings → Basic settings → Background jobs
Or via occ:
docker compose exec -u www-data app \
php occ config:app:get core backgroundjobs_mode
Expected result: cron
