Installing a Full Nextcloud in an LXC Container in Proxmox#
If you enjoyed this article, you can support the author by becoming a sponsor on Boosty (link in the contacts section).
In this video I’ll show you how to install Nextcloud from scratch - without Docker, on a full Apache, MariaDB, and PHP stack. Safe and stable, stylish and modern. The setup I’ll show you today can handle the load of even a small-to-medium office, let alone home use.
But first, I want to thank my sponsors on Boosty. Guys, a huge thank you for helping this channel grow. This channel runs entirely thanks to your support. And your support goes entirely toward growing this channel. For those who don’t know, on Boosty, sponsors get videos early - up to a month before they go public - so if you enjoy the content on this channel, want to support its growth, or maybe just want to watch something ahead of time, the link to Boosty and all my other contacts - Telegram, backup channels on domestic platforms - will be in the description.
I already have two videos on my YouTube channel covering the specifics of installing Nextcloud AIO and Nextcloud in Docker. There are also videos about installing and integrating OnlyOffice with Nextcloud, as well as the Memories gallery. This video will be the final one in the series. But that’s not guaranteed.
Links to Other Installation Options#
In this video we’ll set up a full-featured Nextcloud, but with a few particular twists. First, though, as tradition demands, I need to explain to newcomers what Nextcloud actually is. Nextcloud is an open-source cloud service that lets you store, sync, and share files, and extend its functionality with built-in apps: calendars, video calls, notes, document editors, and so on. So whenever you hear someone say Nextcloud is a replacement for commercial cloud storage, look at them with a bit of pity - like, “oh, you’re still young, you don’t get it yet, rookie.” Nextcloud crossed the line from “just a cloud storage” a long time ago. Today it’s a genuine competitor to solutions like Google Workspace and similar platforms.
Now let’s talk about what’s special about today’s installation. There are only two official Nextcloud builds. That’s the one we’ll install today, and Nextcloud AIO (which, as far as I know, is maintained by essentially one person). Everything else is the work of the community. In videos about installing full Nextcloud, you usually see people setting everything up in a VM, then installing certbot, installing nginx or ngrok, issuing an SSL certificate. We’re not going to do any of that. Why? We already have our own reverse proxy, and that’s what we’ll use. We’ll install everything in an LXC container. Now, the hardcore admins out there are probably yelling at me - “what are you doing, you can’t do that.” You can’t - in production. At home, you can. Why LXC? Because we can pass through the integrated GPU on the CPU, and at the same time still use that same GPU in other containers. To see how GPU passthrough works, check out the video on my channel.
I already have an LXC container created, and my reverse proxy is set up to issue a certificate for the IP address our container lives on, on port 80. I’m assuming you know how to configure your particular reverse proxy. That wraps up the intro.
Let’s get started.
The text of this article has been updated a bit since the video came out: the container is now unprivileged LXC, the data directory can live on an NFS share instead of the local disk, PHP has been bumped to 8.5, and the database - just like in the video - defaults to MariaDB, but an alternative with an external PostgreSQL has been added for anyone who already has one running separately. The overall install logic is the same as in the video; some paths and package versions below have simply been brought up to date.
Step 1. Mounting an NFS Share into the LXC Before Installing Nextcloud#
This step is only needed if Nextcloud’s data (the data directory) should live not on the container’s local disk, but on a separate NFS store - for example, if the disk assigned to the LXC is small, and you’re planning to accumulate a large volume of photos and files. If the container’s local disk works fine for you, as in the video, you can skip this step and go straight to the next one.
This needs to happen before installing Nextcloud, so that the data directory is created directly on the share from the start, instead of on the container’s local filesystem, from where you’d later have to migrate it.
In the container’s config (
/etc/pve/lxc/<CTID>.confon the Proxmox host) or through the GUI: Container → Resources → Add → Mount Point - pick the NFS storage you want, specify the mount point inside the LXC, e.g.:mp0: <storage-id>:<volume-or-path>,mp=/var/www/nextcloud/dataSort out the UID/GID mapping. If the container is unprivileged,
www-datainside the container (UID 33) gets mapped, on the Proxmox host side, to a shifted UID100033- and the NFS server needs to understand who the files actually belong to. Two working options:- Option A - an
lxc.idmapentry in the container’s config, so that UID/GID33specifically doesn’t get shifted when crossing the container boundary:lxc.idmap: u 0 100000 33 lxc.idmap: u 33 33 1 lxc.idmap: u 34 100034 65502 lxc.idmap: g 0 100000 33 lxc.idmap: g 33 33 1 lxc.idmap: g 34 100034 65502 - Option B - leave the container’s IDs alone, and instead set
chown 100033:100033on the data directory ahead of time, on the NFS server’s side.
- Option A - an
After mounting, verify the permissions from inside the container itself:
mkdir -p /var/www/nextcloud/data chown www-data:www-data /var/www/nextcloud/data
Step 2. Update the System#
Update packages and configure the locale:
sudo apt update && sudo apt upgrade -ysudo dpkg-reconfigure localesStep 3. Install Apache2 and the PHP Modules#
Install Apache2:
sudo apt install apache2 -yInstall the dependencies - note that the list includes both database drivers at once, php-mysql (for MariaDB, as in the video) and php-pgsql (in case you go with the PostgreSQL alternative in the next step) - having the extra one doesn’t hurt anything:
sudo apt install php php-common libapache2-mod-php php-bz2 php-gd php-mysql php-pgsql \
php-curl php-mbstring php-imagick php-zip php-xml php-json php-bcmath \
php-intl php-gmp zip unzip wget smbclient libmagickcore-7.q16-10-extra ffmpeg -yEnable the required Apache modules:
sudo a2enmod env rewrite dir mime headers setenvif sslRestart, enable at boot, and check that Apache is working:
sudo systemctl restart apache2
sudo systemctl enable apache2
sudo systemctl status apache2Check which modules are loaded:
sudo apache2ctl -MStep 4. Database: MariaDB (as in the Video) or an External PostgreSQL#
The default option - MariaDB inside the same container#
Install the package:
sudo apt install mariadb-server -yLog into MariaDB:
sudo mysqlCreate the database and user for Nextcloud, and grant permissions:
CREATE USER 'ncloud'@'localhost' IDENTIFIED BY 'YOUR_PASSWORD';
CREATE DATABASE ncloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
GRANT ALL PRIVILEGES ON ncloud.* TO 'ncloud'@'localhost';
FLUSH PRIVILEGES;
quit;Restart and enable MariaDB at boot:
sudo systemctl restart mariadb
sudo systemctl enable mariadbCheck that the service is running:
sudo systemctl status mariadbAlternative - an external PostgreSQL. If, instead of a local MariaDB, you’d rather put the database on a separate, already-running PostgreSQL server (say, the same one used by other services), the step looks different, and you skip installing MariaDB entirely:
- Login/Group Role (the database user) in pgAdmin: right-click Login/Group Roles → Create → Login/Group Role → General → Name:
ncloud→ Definition → Password: set a password → Privileges → enable Can login? (required). - Database: right-click Databases → Create → Database → General → Database:
nextcloud→ Owner: pickncloud- it automatically gets full rights on this database. - Check the encoding - on the General/Advanced tab it should be
UTF8(Nextcloud requires it). - On the Postgres server’s side, check:
listen_addressesinpostgresql.confneeds to listen on more than justlocalhost(grep listen_addresses /etc/postgresql/*/main/postgresql.conf, change to*andsystemctl restart postgresqlif needed), andpg_hba.confneeds to allow connections from the Nextcloud container’s IP. Plus a firewall rule on TCP port5432between the Nextcloud container and the Postgres server.
The Nextcloud install command in step 6 will also differ in this case - see the box there.
Step 5. Download Nextcloud#
cd /var/www/
wget https://download.nextcloud.com/server/releases/latest.zip
unzip latest.zip
rm -rf latest.zipchown -R www-data:www-data /var/www/nextcloud/Step 6. Install Nextcloud from the Command Line#
For the default MariaDB setup:
cd /var/www/nextcloud
sudo -u www-data php occ maintenance:install --database \
"mysql" --database-name "ncloud" --database-user "ncloud" --database-pass \
'YOUR_PASSWORD' --data-dir "/var/www/nextcloud/data" \
--admin-user "stilicho" --admin-pass "YOUR_ADMIN_PASSWORD"If you went with the external PostgreSQL alternative instead of MariaDB (step 4), the command is almost the same, but with pgsql instead of mysql and a required --database-host:
cd /var/www/nextcloud
sudo -u www-data php occ maintenance:install --database \
"pgsql" --database-host "POSTGRES_IP_OR_HOST" --database-name "nextcloud" \
--database-user "ncloud" --database-pass \
'YOUR_PASSWORD' --data-dir "/var/www/nextcloud/data" \
--admin-user "stilicho" --admin-pass "YOUR_ADMIN_PASSWORD"If you mounted an NFS share for data in step 1, --data-dir in both variants is explicitly pointed at the path where it’s mounted - if you skip this, Nextcloud will create data on the container’s local disk, and you’ll have to migrate it by hand later.
Step 7. Edit config.php#
nano /var/www/nextcloud/config/config.php'trusted_domains' =>
array (
0 => 'localhost',
1 => 'next.stilicho.ru',
),'overwritehost' => 'next.stilicho.ru',
'overwriteprotocol' => 'https',
'overwrite.cli.url' => 'https://next.stilicho.ru',
'trusted_proxies' =>
array (
0 => '172.16.0.0/12',
1 => '192.168.0.0/16',
2 => '10.0.0.0/8',
3 => 'fc00::/7',
4 => 'fe80::/10',
5 => '2001:db8::/32',
),
'default_phone_region' => 'RU',
'allow_local_remote_servers' => trueStep 8. Configure Apache (Initial VirtualHost)#
nano /etc/apache2/sites-enabled/000-default.conf<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/nextcloud
<Directory /var/www/nextcloud>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ServerName next.stilicho.ru
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains"
</IfModule>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>Step 9. Switch to PHP-FPM#
apt install php8.5-fpmservice php8.5-fpm status
php-fpm8.5 -v
ls -la /var/run/php/php8.5-fpm.socka2dismod php8.5
a2dismod mpm_preforka2enmod mpm_event proxy_fcgi setenvif
a2enconf php8.5-fpmsystemctl restart apache2PHP settings for large files#
sed -i 's/^upload_max_filesize.*/upload_max_filesize = 16G/; s/^post_max_size.*/post_max_size = 16G/; s/^memory_limit.*/memory_limit = 512M/; s/^max_execution_time.*/max_execution_time = 600/; s/^;max_input_vars.*/max_input_vars = 3000/; s/^max_input_time.*/max_input_time = 1000/' /etc/php/8.5/fpm/php.inior for very large files:
sed -i 's/^upload_max_filesize.*/upload_max_filesize = 16G/; s/^post_max_size.*/post_max_size = 16G/; s/^memory_limit.*/memory_limit = 2048M/; s/^max_execution_time.*/max_execution_time = 3600/; s/^;max_input_vars.*/max_input_vars = 3600/; s/^max_input_time.*/max_input_time = 3600/' /etc/php/8.5/fpm/php.iniPHP-FPM pool settings#
sed -i 's/^pm.max_children = .*/pm.max_children = 64/; s/^pm.start_servers = .*/pm.start_servers = 16/; s/^pm.min_spare_servers = .*/pm.min_spare_servers = 16/; s/^pm.max_spare_servers = .*/pm.max_spare_servers = 32/' /etc/php/8.5/fpm/pool.d/www.confor:
sed -i 's/^pm.max_children = .*/pm.max_children = 70/; s/^pm.start_servers = .*/pm.start_servers = 20/; s/^pm.min_spare_servers = .*/pm.min_spare_servers = 20/; s/^pm.max_spare_servers = .*/pm.max_spare_servers = 60/' /etc/php/8.5/fpm/pool.d/www.confservice php8.5-fpm restartGateway timeout during a large simultaneous upload. If a lot of data gets uploaded at once from several sources (say, phone + computer at the same time), all of the pm.max_children workers can end up busy in parallel (every upload plus the preview/blurhash generation for each file holds onto a worker), and Apache starts returning 502/504 Gateway Timeout. The quick fix is to reset the stuck queue:
systemctl restart php8.5-fpmCheck the number of active processes:
ps aux | grep php-fpm | wc -lIf that number is close to pm.max_children, that confirms it’s an overload, not a broken config. If this happens regularly, you can raise pm.max_children further (keep an eye on free -h so you don’t tip into swap), or just spread large uploads out over time.
Update the VirtualHost for FPM#
nano /etc/apache2/sites-enabled/000-default.conf<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/nextcloud
<Directory /var/www/nextcloud>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
<FilesMatch ".php$">
SetHandler "proxy:unix:/var/run/php/php8.5-fpm.sock|fcgi://localhost/"
</FilesMatch>
ServerName next.stilicho.ru
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains"
</IfModule>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>systemctl restart apache2Verify FPM via info.php (delete it after checking!)#
cd /var/www/nextcloud
sudo nano info.php<?php phpinfo(); ?>
Open https://next.stilicho.ru/info.php - it should show “Server API: FPM/FastCGI”.
Step 10. OPCache#
nano /etc/php/8.5/fpm/conf.d/10-opcache.inizend_extension=opcache.so
opcache.enable=1
opcache.enable_cli=1
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=12000
opcache.memory_consumption=512
opcache.save_comments=1
opcache.revalidate_freq=60
opcache.jit=on
opcache.jit = 1255
opcache.jit_buffer_size = 256Mservice php8.5-fpm restartStep 11. APCu#
apt install php8.5-apcunano /etc/php/8.5/fpm/conf.d/20-apcu.iniextension=apcu.so
apc.enable_cli=1
apc.shm_size=128MThe default apc.shm_size is 32M - for an instance running Memories/recognize with a large file library, that’s not enough, and the cache fills up quickly (you’ll see a warning in the “Security & setup warnings” check). Set it to 128M right away, and raise it further later if needed.
systemctl restart php8.5-fpm
systemctl restart apache2Check info.php - “APCu support Enabled”.
nano /var/www/nextcloud/config/config.php'memcache.local' => '\OC\Memcache\APCu',Step 12. Redis (Cache + File Locking)#
apt install redis-server php-redis -ysystemctl start redis-server
systemctl enable redis-servernano /etc/redis/redis.confport 0
unixsocket /var/run/redis/redis.sock
unixsocketperm 770usermod -aG redis www-datanano /var/www/nextcloud/config/config.php'filelocking.enabled' => 'true',
'memcache.distributed' => '\\OC\\Memcache\\Redis',
'memcache.locking' => '\\OC\\Memcache\\Redis',
'redis' => [
'host' => '/var/run/redis/redis.sock',
'port' => 0,
'dbindex' => 0,
'password' => '',
'timeout' => 1.5,
],nano /etc/php/8.5/fpm/php.iniredis.session.locking_enabled=1
redis.session.lock_retries=-1
redis.session.lock_wait_time=10000systemctl restart redis-server
systemctl restart php8.5-fpm
systemctl restart apache2Step 13. Pretty URLs#
ffmpeg was already installed in step 3, no need to install it again.
nano /var/www/nextcloud/config/config.php'htaccess.RewriteBase' => '/',sudo -u www-data php --define apc.enable_cli=1 /var/www/nextcloud/occ maintenance:update:htaccessStep 14. Final occ Commands#
sudo -u www-data php /var/www/nextcloud/occ maintenance:repair --include-expensive
sudo -u www-data php /var/www/nextcloud/occ db:add-missing-indicessudo -u www-data php /var/www/nextcloud/occ config:system:set maintenance_window_start --type=integer --value=1sudo crontab -u www-data -e*/5 * * * * php -f /var/www/nextcloud/cron.phpsudo -u www-data php /var/www/nextcloud/occ recognize:download-models
sudo -u www-data php /var/www/nextcloud/occ memories:places-setup
sudo -u www-data php /var/www/nextcloud/occ memories:index
sudo -u www-data php /var/www/nextcloud/occ files:scan --unscanned --allStep 15. notify_push#
First, install the notify_push app itself from the App Store - ready-made binaries for various architectures come with it:
sudo -u www-data php /var/www/nextcloud/occ app:install notify_pushCopy the binary for your architecture (for x86_64/amd64):
cp /var/www/nextcloud/apps/notify_push/bin/x86_64/notify_push /usr/local/bin/
chown root:root /usr/local/bin/notify_push
chmod 755 /usr/local/bin/notify_pushIf your architecture is ARM (say, for an ARM mini-PC), the path is apps/notify_push/bin/aarch64/notify_push. Check available options with: ls /var/www/nextcloud/apps/notify_push/bin/.
nano /etc/systemd/system/notify_push.service[Unit]
Description=Push daemon for Nextcloud clients
Documentation=https://github.com/nextcloud/notify_push
After=network.target redis.service php8.5-fpm.service apache2.service
Requires=redis.service
[Service]
Environment=PORT=7867
Environment=NEXTCLOUD_URL=https://next.stilicho.ru
ExecStart=/usr/local/bin/notify_push /var/www/nextcloud/config/config.php
User=www-data
Group=www-data
Type=notify
Restart=always
RestartSec=5
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
ProtectHome=read-only
[Install]
WantedBy=multi-user.targetmariadb.service has been removed from After= - in this setup, if you’re using an external database, there’s no local DB service in this container.
systemctl daemon-reload
systemctl start notify_push
systemctl enable notify_push
systemctl status notify_pushnano /var/www/nextcloud/config/config.php'push' => array(
'base_endpoint' => 'https://next.stilicho.ru/push',
),sudo -u www-data php /var/www/nextcloud/occ notify_push:setup http://127.0.0.1:7867If you run the command without an address, the wizard often returns default port(7867) is in use, because the service is already running (this isn’t an error, it’s the normal state - just specify the address explicitly, as above).
Check the current status at any time:
sudo -u www-data php /var/www/nextcloud/occ notify_push:self-testEvery item should be green except using unencrypted http for push server - that’s expected for a localhost + external HTTPS-via-Traefik setup, and not a problem.
Step 16. trusted_proxies and forwarded_for_headers (Final Fix for the Real Topology)#
Since Traefik sits in front of the LXC (and possibly one more node before that), trusted_proxies alone isn’t enough: without forwarded_for_headers, Nextcloud doesn’t know which header to pull the client’s real IP from, and sees Traefik’s own IP instead. This causes false positives in the brute-force protection (every client “looks like” the same IP).
nano /var/www/nextcloud/config/config.php'trusted_proxies' =>
array (
0 => '172.16.0.0/12',
1 => '192.168.0.0/16',
2 => '10.0.0.0/8',
3 => 'fc00::/7',
4 => 'fe80::/10',
5 => '2001:db8::/32',
6 => '127.0.0.1',
7 => '::1',
),
'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR'],systemctl restart apache2
systemctl restart php8.5-fpmIf, after this, nextcloud.log still shows the proxy’s IP instead of the real client - reset the current lockout on the proxy’s IP:
sudo -u www-data php /var/www/nextcloud/occ security:bruteforce:reset PROXY_IPTrusted proxy for Traefik (important for notify_push and general correctness)#
If your static Traefik config uses trustedIPs (say, a YAML anchor with a list of Cloudflare ranges), add the IPs of internal nodes in the chain (e.g. an entry proxy sitting in front of the main Traefik) - otherwise X-Forwarded-For will get truncated, and notify_push (and Nextcloud itself) won’t see the real IP:
entryPoints:
web:
address: ":80"
forwardedHeaders:
trustedIPs: &trustedIps
# ...existing ranges (e.g. Cloudflare)...
- 192.168.x.x/32 # internal node in front of Traefik - plug in your own IP
- 127.0.0.1/32
websecure:
address: ":443"
forwardedHeaders:
trustedIPs: *trustedIpsRestart Traefik after making the change.
Step 17. Memories (Photos/Video) + go-vod Transcoding#
sudo -u www-data php /var/www/nextcloud/occ app:install memoriesThere’s no need to install go-vod separately for video transcoding (HLS) - on bare metal (without Docker), Memories downloads and runs its own bundled binary as long as ffmpeg is present on the host (already installed in step 3). Enable it under: Settings → Administration → Memories → Video streaming → Enable transcoding.
Reverse geocoding (based on OpenStreetMap data, uses Postgres’s native geometry support):
sudo -u www-data php /var/www/nextcloud/occ memories:places-setupStep 18. Preview Generator (Background Preview Generation)#
sudo -u www-data php /var/www/nextcloud/occ app:install previewgenerator
sudo -u www-data php /var/www/nextcloud/occ config:app:set previewgenerator squareSizes --value="32 256"
sudo -u www-data php /var/www/nextcloud/occ config:app:set previewgenerator widthSizes --value="256 384 512 1024 2048 4096"
sudo -u www-data php /var/www/nextcloud/occ config:app:set previewgenerator heightSizes --value="256 384 512 1024 2048 4096"
sudo -u www-data php /var/www/nextcloud/occ preview:generate-allKnown bug on version 34.0.3.x (current as of this revision of the article): preview:generate-all/pre-generate fail with an Ambiguous column: file_id error on PostgreSQL (and on MariaDB too). The bug is confirmed and already fixed in source (stable34), but the fix will only land in 34.0.4, which hasn’t shipped as a final release yet at the time of writing. Until it does, background pre-generation doesn’t work, and previews get generated on demand the first time a file is opened (slower, but not a big deal). You can’t update via occ upgrade until Nextcloud itself sees the new version - you’ll need to manually download the new archive from download.nextcloud.com and replace the files (rsync --exclude=config --exclude=data) once 34.0.4 is out.
Add this to the same cron as cron.php (step 14):
sudo crontab -u www-data -e*/10 * * * * php -f /var/www/nextcloud/occ preview:pre-generateProblem: previews for HEIC photos (iPhone) don’t get generated#
If some of your files are iPhone photos in .HEIC format, nextcloud.log may start filling up with something like this (especially if the recognize app is also enabled):
Error core ImagickException
Decoder plugin generated an error: Unspecified (7.0) `/path/to/file.HEIC' @ error/heic.c/IsHEIFSuccess/202
Warning recognize
Failed to generate preview of <id> with dimension 1024 with gdlib: Could not load image for preview with gdlibTo diagnose, log into the server and try decoding the file directly, bypassing Nextcloud/PHP:
sudo apt install imagemagick -y
identify -list format | grep -i heicIf the format shows up in the list (usually marked r--, i.e. read-only), a delegate technically exists, but decoding can still fail. Check a specific file:
identify -verbose '/path/to/file.HEIC' 2>&1 | head -30If you get the same IsHEIFSuccess error, the cause is almost certainly a missing HEVC decoder for libheif. The thing is, HEIC from an iPhone is encoded with HEVC (H.265), while the libheif1 package from Ubuntu’s default repository only pulls in AV1 plugins (libheif-plugin-aomdec/aomenc, needed for AVIF) - there’s no HEVC decoder among them:
dpkg -l | grep -i heifCheck for and install the missing decoder:
apt-cache search libheif-plugin
sudo apt install libheif-plugin-libde265 -yOr, to avoid guessing which specific encoding variant different iPhone models use, install every plugin in one package:
sudo apt install libheif-plugins-all -yVerify the file is now readable (no errors, with correct dimensions):
identify '/path/to/file.HEIC'Restart the web stack and regenerate previews:
sudo systemctl restart php8.5-fpm apache2
sudo -u www-data php /var/www/nextcloud/occ preview:generate-allphp-imagick uses the system’s libmagickcore, which in turn dynamically loads libheif1 - there’s no need to reinstall imagemagick/php-imagick itself, just installing the missing decoder plugin and restarting PHP-FPM is enough.





