<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Артем Ахмадеев</title>
    <description>The latest articles on DEV Community by Артем Ахмадеев (@__4b740f75df).</description>
    <link>https://dev.to/__4b740f75df</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4237045%2F7e371d5a-d257-4985-99da-2d59074ebf5e.png</url>
      <title>DEV Community: Артем Ахмадеев</title>
      <link>https://dev.to/__4b740f75df</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/__4b740f75df"/>
    <language>en</language>
    <item>
      <title>Run Your Own Private Cloud: Nextcloud on Docker with Backups</title>
      <dc:creator>Артем Ахмадеев</dc:creator>
      <pubDate>Fri, 13 Nov 2026 08:58:40 +0000</pubDate>
      <link>https://dev.to/__4b740f75df/run-your-own-private-cloud-nextcloud-on-docker-with-backups-1a9</link>
      <guid>https://dev.to/__4b740f75df/run-your-own-private-cloud-nextcloud-on-docker-with-backups-1a9</guid>
      <description>&lt;p&gt;Cloud storage is convenient, but your files live on someone else's servers, under someone else's rules and pricing. Free tiers fill up quickly, and handing personal documents, photos and backups to a third party is a trade-off many developers would rather avoid.&lt;/p&gt;

&lt;p&gt;In this guide we build a private Nextcloud backed by PostgreSQL, running in Docker on your own VPS. It restarts automatically after a reboot, stays off the public internet and is reached through an SSH tunnel. We also test file sync over WebDAV and set up a nightly backup job. Self-hosting gives you full control over your data, predictable costs and a setup you can fully understand and reproduce.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/KgVJjIxZd6U" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A VPS with Ubuntu 24.04 (2 vCPU / 4 GB RAM is plenty). I use &lt;a href="https://www.vdsina.com/?partner=xuevxsn1ece5" rel="noopener noreferrer"&gt;VDSina&lt;/a&gt; &lt;em&gt;(affiliate link — it supports the channel at no extra cost to you)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;An SSH connection to it (any terminal works).&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Short on time? Every command below is also in &lt;a href="https://github.com/lamplightservers/lamplightservers.github.io/blob/main/scripts/run-your-own-private-cloud-nextcloud-on-docker-with-backups/install.sh" rel="noopener noreferrer"&gt;one ready-to-run script&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: Update the system
&lt;/h2&gt;

&lt;p&gt;We start by updating the system packages so we're building on a clean, current base. We also install cron, which the nightly backup needs later, and enable it so it starts on every boot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="kd"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive
apt-get update -y
apt-get upgrade -y -o Dpkg::Options::=--force-confold
apt-get install -y cron
systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now cron
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Install Docker
&lt;/h2&gt;

&lt;p&gt;Next we install Docker with the official convenience script. It sets up the Docker engine and the Compose plugin, which lets us run Nextcloud and its database as containers instead of installing them by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -fsSL https://get.docker.com &lt;span class="o"&gt;|&lt;/span&gt; sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Generate secrets file
&lt;/h2&gt;

&lt;p&gt;We create a project folder and a secrets file holding the database and admin passwords. The passwords are generated randomly with OpenSSL so they can't be guessed, and the file permissions are tightened so only root can read it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkdir -p /opt/nextcloud &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /opt/nextcloud
cat &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;EOF
&lt;span class="nv"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;openssl rand -hex 16&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;NC_ADMIN_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin
&lt;span class="nv"&gt;NC_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;openssl rand -hex 12&lt;span class="o"&gt;)&lt;/span&gt;
EOF
chmod 600 .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Write the compose file
&lt;/h2&gt;

&lt;p&gt;The compose file defines two services, PostgreSQL and Nextcloud, both set to restart unless stopped so they return after a reboot. Nextcloud waits for the database health check before starting and listens only on localhost, so it isn't exposed to the internet. A config check at the end confirms the file is valid.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cat &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /opt/nextcloud/docker-compose.yml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s1"&gt;'EOF'&lt;/span&gt;
name: nextcloud
services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: &lt;span class="nv"&gt;${DB_PASSWORD}&lt;/span&gt;
    volumes:
      - db:/var/lib/postgresql/data
    healthcheck:
      test: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CMD-SHELL"&lt;/span&gt;, &lt;span class="s2"&gt;"pg_isready -U nextcloud -d nextcloud"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      interval: 5s
      timeout: 5s
      retries: 20
  app:
    image: nextcloud:apache
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - &lt;span class="s2"&gt;"127.0.0.1:8080:80"&lt;/span&gt;
    environment:
      POSTGRES_HOST: db
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: &lt;span class="nv"&gt;${DB_PASSWORD}&lt;/span&gt;
      NEXTCLOUD_ADMIN_USER: &lt;span class="nv"&gt;${NC_ADMIN_USER}&lt;/span&gt;
      NEXTCLOUD_ADMIN_PASSWORD: &lt;span class="nv"&gt;${NC_ADMIN_PASSWORD}&lt;/span&gt;
      NEXTCLOUD_TRUSTED_DOMAINS: localhost 127.0.0.1
    volumes:
      - app:/var/www/html
volumes:
  db:
  app:
EOF
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/nextcloud &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose config -q &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; compose-ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Start Nextcloud and create admin
&lt;/h2&gt;

&lt;p&gt;We bring everything up with docker compose. Docker pulls the images, starts the database, then starts Nextcloud, which creates the admin account on its first run. That first start can take a minute or two, so be patient.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/nextcloud &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Show login and phone info
&lt;/h2&gt;

&lt;p&gt;This step prints the login details. Since the port is bound to localhost, you open an SSH tunnel from your own computer and browse to localhost on port 8080 to sign in with the generated credentials. The phone app works the same way, but only through a tunnel or a VPN.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;. /opt/nextcloud/.env
&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;hostname -I &lt;span class="o"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Tunnel from your PC: ssh -N -L 8080:localhost:8080 root@$IP"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Then open: http://localhost:8080"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"User: $NC_ADMIN_USER  Password: $NC_ADMIN_PASSWORD"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Phone: install the official Nextcloud app (Google Play, F-Droid or App Store) and log in to this server."&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Note: a phone can only reach it through a tunnel or VPN, since port 8080 is bound to localhost only."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: Upload and verify a test file
&lt;/h2&gt;

&lt;p&gt;To test sync for real, we create a small text file and upload it over WebDAV, the same protocol sync clients use. If the command finishes without an error the upload worked, and the file shows up in the web interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;. /opt/nextcloud/.env
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Lamplight sync works"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/test.txt
curl -s -u &lt;span class="s2"&gt;"$NC_ADMIN_USER:$NC_ADMIN_PASSWORD"&lt;/span&gt; -T /tmp/test.txt http://localhost:8080/remote.php/dav/files/&lt;span class="nv"&gt;$NC_ADMIN_USER&lt;/span&gt;/test.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Write the backup script
&lt;/h2&gt;

&lt;p&gt;The backup script puts Nextcloud into maintenance mode, dumps the database and archives the files volume. It turns maintenance mode off again on exit, even if something fails, so the instance isn't left locked. Backups older than seven days are deleted so the disk doesn't fill up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cat &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /usr/local/bin/nc-backup.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s1"&gt;'EOF'&lt;/span&gt;
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; -e
&lt;span class="nv"&gt;B&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/backups/nextcloud
&lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;date +%F-%H%M&lt;span class="o"&gt;)&lt;/span&gt;
mkdir -p &lt;span class="s2"&gt;"$B"&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/nextcloud
occ&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; -T -u www-data app php occ &lt;span class="s2"&gt;"$@"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
occ maintenance:mode --on
&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'occ maintenance:mode --off'&lt;/span&gt; EXIT
docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; -T db sh -c &lt;span class="s1"&gt;'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; gzip &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"$B/db-$D.sql.gz"&lt;/span&gt;
docker run --rm -v nextcloud_app:/data:ro -v &lt;span class="s2"&gt;"$B"&lt;/span&gt;:/backup alpine tar czf &lt;span class="s2"&gt;"/backup/files-$D.tar.gz"&lt;/span&gt; -C /data .
find &lt;span class="s2"&gt;"$B"&lt;/span&gt; -type f -mtime +7 -delete
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"backup $D done"&lt;/span&gt;
EOF
chmod +x /usr/local/bin/nc-backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 9: Schedule nightly backup
&lt;/h2&gt;

&lt;p&gt;We schedule the script with a cron file so it runs every night at three o'clock. Its output goes to a log file, which lets you check later that it actually ran.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cat &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/cron.d/nextcloud-backup &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s1"&gt;'EOF'&lt;/span&gt;
0 3 * * * root /usr/local/bin/nc-backup.sh &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/log/nc-backup.log 2&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt;1
EOF
chmod 644 /etc/cron.d/nextcloud-backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 10: Run backup and verify
&lt;/h2&gt;

&lt;p&gt;Finally we run the backup by hand to prove it works. The listing should show a compressed database dump and a files archive. A backup you haven't tested is just a hope, so never skip this check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/local/bin/nc-backup.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ls -lh /var/backups/nextcloud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Exposing the Nextcloud port publicly without HTTPS sends logins and files in plain text. Keep it bound to localhost and use an SSH tunnel or VPN, or put a properly configured TLS reverse proxy in front of it.&lt;/li&gt;
&lt;li&gt;Leaving the backups on the same server means a disk failure or a compromised VPS takes both the data and its copies. Regularly copy the archives to another machine or off-site storage.&lt;/li&gt;
&lt;li&gt;Losing or exposing the secrets file causes trouble either way. Keep it readable only by root, store a copy somewhere safe, and don't commit it to Git, since the database password can't easily be recovered.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Now that your Nextcloud is running and backed up, try restoring a backup on a test machine to be sure it works. Then add a domain with HTTPS, install the desktop and mobile sync clients, and consider off-site copies of your backups.&lt;/p&gt;

&lt;p&gt;Every command here was tested on a fresh Ubuntu 24.04 server before publishing. Watch the full walkthrough on &lt;a href="https://youtu.be/KgVJjIxZd6U" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt; — new tutorials every week on &lt;strong&gt;Lamplight Servers&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>nextcloud</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
