...

How to Deploy Nextcloud on a VPS with PostgreSQL, Redis, and HTTPS

Martin Klein

Reading time 1 minute

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:

  1. A PostgreSQL dump;
  2. The Nextcloud configuration;
  3. 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:

VolumePath inside the containerContents
nextcloud_postgres/var/lib/postgresql/dataPostgreSQL database
nextcloud_postgres/var/www/htmlNextcloud application, configuration, and additional apps
nextcloud_data/var/www/html/datauser 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:

NAMEIMAGESTATUSPORTS
nextcloud-appnextcloud:apacheUp127.0.0.1:8080->80/tcp
nextcloud-cronnextcloud:apacheUp
nextcloud-dbpostgres:17-alpineUp (healthy)5432/tcp
nextcloud-redisredis:alpineUp (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:

DRIVERVOLUME NAME
localnextcloud_app
localnextcloud_data
localnextcloud_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:

ParameterValue
TypeA
Namewordpress
IPv4 address203.0.113.10
TTLAuto 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:

  1. Check whether a new version is available;
  2. Review the release notes;
  3. Verify app compatibility;
  4. Create a full backup;
  5. 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 &quot;$POSTGRES_USER&quot; -d &quot;$POSTGRES_DB&quot; -c &quot;\dt&quot;

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 &quot;Docker: &quot;

systemctl is-active docker

printf &quot;Nginx: &quot;

systemctl is-active nginx

Both services should return: active

Make sure they are enabled to start automatically:

printf &quot;Docker autostart: &quot;

systemctl is-enabled docker

printf &quot;Nginx autostart: &quot;

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 \

&quot;table {{.Name}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}&quot;

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 &quot;Docker: &quot;

systemctl is-active docker

printf &quot;Nginx: &quot;

systemctl is-active nginx

echo

docker compose ps –format \

&quot;table {{.Name}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}&quot;

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 &quot;$REDIS_PASSWORD&quot; ping’

Expected response: PONG

Check the PostgreSQL connection:

set -a

source .env

set +a

docker compose exec db \

pg_isready -U &quot;$POSTGRES_USER&quot; -d &quot;$POSTGRES_DB&quot;

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

Sources

  1. Nextcloud Docker Image — official repository and configuration reference
  2. Nextcloud Administration Manual — Memory caching
  3. Nextcloud Administration Manual — Background jobs
  4. Nextcloud Administration Manual — How to upgrade

Subscribe to our newsletter and receive articles and news

    Check out our other materials