👍 What we like
- ✓Gives existing hardware a second life.
- ✓Automatic HTTPS with Caddy in a few lines of config.
- ✓Nextcloud only listens on 127.0.0.1 and stays behind the reverse proxy.
- ✓Updates and backups are simple with Docker Compose.
👎 What to watch
- ✕You need a public IPv4 address, which doesn't work behind CGNAT without a tunnel.
- ✕Maintenance (updates, backups, disk health) is up to you.
- ✕An old PC or mini PC has few bays for a real multi-drive NAS.
- ✕Ports published by Docker bypass UFW and need care.
🏆 Our picks
Affiliate links · same price for youLenovo ThinkCentre M720q Tiny
Amazon · price and availability on the listing
See all our recommended gear in the shop.
📑 Contents ▾
- 01 Prerequisites
- 02 Step-by-step installation
- · Step 1: Create the USB installer
- · Step 2: Install Ubuntu Server
- · Step 3: Network, DNS and port forwarding
- · Step 4: Install Docker Engine from the official repository
- · Step 5: Deploy Nextcloud and MariaDB with Docker Compose
- · Step 6: Install Caddy and enable HTTPS
- · Step 7: Firewall, background jobs and reboot test
- 10 Verification checks
- 11 Security and best practices
- 12 Troubleshooting common errors
- 13 FAQ
- · Why Caddy rather than Nginx and Certbot?
- · Can I use Debian instead of Ubuntu?
- · Can I turn it into a NAS?
- · What does it cost?
- 18 Read also
- 19 Host this yourself
An old desktop or laptop sitting in a cupboard can still make a good home server. This tutorial walks through a setup that works in 2026: Ubuntu Server LTS (24.04 or 26.04), Docker Engine from Docker’s official repository, Nextcloud with a MariaDB database managed by Docker Compose, and Caddy as the reverse proxy. Caddy gets and renews the Let’s Encrypt HTTPS certificate on its own. You don’t need Certbot, and the old certbot-auto script no longer exists.
The idea is simple. Only Caddy listens on the Internet (ports 80 and 443). Nextcloud only listens on 127.0.0.1:8080. SSH stays limited to your local network.
Prerequisites
- A working 64-bit PC. Nextcloud strongly recommends a 64-bit CPU, OS and PHP, and about 512 MB of RAM per PHP process. An SSD is better than a hard drive for the system. A few GB of RAM leaves room if you add more services later.
- No old PC around? Refurbished mini PCs such as the Lenovo ThinkCentre M720q Tiny or the HP ProDesk 600 G5 Mini work well for this tutorial: they’re compact, x86-64 and quiet. However, they have very few storage slots. They are not multi-drive NAS boxes, and you can’t run RAID across several 3.5” drives in them. Check the spec sheet of your exact configuration.
- A USB stick of at least 4 GB for the installer. Everything on it will be erased.
- A domain name (or a subdomain, such as
cloud.example.com) whose DNS zone you manage. - A reachable public IPv4 address. If your ISP puts you behind CGNAT, port forwarding won’t work. Ask your ISP for a full public IPv4 address or use a tunnel.
- Access to your router’s interface to create a DHCP reservation and forward TCP ports 80 and 443. Forward nothing else, and certainly not port 22.
Step-by-step installation
Step 1: Create the USB installer
Download the Ubuntu Server LTS ISO from ubuntu.com, version 24.04 LTS or 26.04 LTS. Ubuntu 20.04 has had no standard support since May 2025, so don’t use it. Check the image’s integrity first (the SHA256SUMS file is on the release’s download page):
sha256sum -c SHA256SUMS --ignore-missing
The output must show OK next to the ISO name.
⚠️ Warning: the dd command below wipes the whole target device. Make sure you’ve identified the stick correctly first. Plug it in, run lsblk, then compare its size (SIZE), model (MODEL) and transport (TRAN = usb):
lsblk -o NAME,SIZE,MODEL,TRAN,MOUNTPOINTS
# Replace /dev/sdX with the stick identified above (NOT your system disk)
sudo dd if=ubuntu-*-live-server-amd64.iso of=/dev/sdX bs=4M status=progress conv=fsync
Check: run lsblk again. The stick should now contain the installer’s partitions.
Step 2: Install Ubuntu Server
Boot the PC from the stick (F12, F10 or Esc depending on the manufacturer). In the installer:
- Pick the target disk. ⚠️ “Use an entire disk” wipes that disk. Back up anything you need first, and check the model and size shown.
- Create your user. It is not
root: every admin command will go throughsudo. - Tick Install OpenSSH server.
After the reboot, log in and update the system:
sudo apt update && sudo apt full-upgrade -y
sudo apt install -y smartmontools dnsutils
Check: confirm the installed version, then the hardware health. The disk should report PASSED on the SMART test (use /dev/sda or /dev/nvme0n1 depending on lsblk):
lsb_release -d
lsblk -o NAME,SIZE,MODEL,TYPE
free -h
sudo smartctl -H /dev/sda
sudo dmesg --level=err,crit,alert,emerg
Ubuntu Server turns on automatic security updates by default. To confirm:
systemctl is-active unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades
The service should be active, and both lines in the file should be set to "1".
Step 3: Network, DNS and port forwarding
- On your router, create a DHCP reservation for the server’s MAC address. It’s simpler and less error-prone than a static IP in netplan. Note its local address:
ip -4 addr show
- At your registrar, create an A record for
cloud.example.comthat points to your router’s public IPv4 address (shown in its interface). - On the router, forward only TCP ports 80 and 443 to the server’s local IP.
Check: DNS should return your public IP:
dig +short A cloud.example.com
Step 4: Install Docker Engine from the official repository
Ubuntu’s docker.io package doesn’t ship the docker compose plugin used below. If you installed it before, remove it first (sudo apt remove docker.io). Here is the procedure from Docker’s documentation:
sudo apt update
sudo apt install -y ca-certificates curl
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
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
Check:
systemctl is-enabled docker # should print: enabled
sudo docker run --rm hello-world # should print "Hello from Docker!"
sudo docker compose version
Step 5: Deploy Nextcloud and MariaDB with Docker Compose
Create the project folder and a .env file with random passwords that only root can read:
sudo mkdir -p /opt/nextcloud && cd /opt/nextcloud
sudo tee .env >/dev/null <<EOF
MYSQL_ROOT_PASSWORD=$(openssl rand -base64 24)
MYSQL_PASSWORD=$(openssl rand -base64 24)
NC_ADMIN_PASSWORD=$(openssl rand -base64 24)
NC_DOMAIN=cloud.example.com
EOF
sudo chmod 600 .env
Replace cloud.example.com with your domain (sudo nano .env). Then create /opt/nextcloud/compose.yaml. The nextcloud image reads MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD and MYSQL_HOST. If any of these four is missing, it falls back to SQLite.
services:
db:
image: mariadb:lts
restart: always
command: --transaction-isolation=READ-COMMITTED
volumes:
- ./db:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
app:
image: nextcloud:stable
restart: always
ports:
- "127.0.0.1:8080:80" # reachable from the server only (Caddy)
depends_on:
- db
volumes:
- ./nextcloud:/var/www/html
environment:
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_HOST=db
- NEXTCLOUD_ADMIN_USER=admin
- NEXTCLOUD_ADMIN_PASSWORD=${NC_ADMIN_PASSWORD}
- NEXTCLOUD_TRUSTED_DOMAINS=${NC_DOMAIN}
- TRUSTED_PROXIES=172.16.0.0/12
- OVERWRITEPROTOCOL=https
- OVERWRITECLIURL=https://${NC_DOMAIN}
restart: always restarts the containers after the server reboots. TRUSTED_PROXIES=172.16.0.0/12 covers the gateway of Docker’s default networks, which is where requests relayed by Caddy come from. Change this range if you’ve customised your Docker networks.
cd /opt/nextcloud
sudo docker compose up -d
sudo docker compose logs -f app # Ctrl+C once initialisation is done
Check: both containers should be Up, and occ status should report installed: true. The first install can take a minute or two.
sudo docker compose ps
sudo docker compose exec -u www-data app php occ status
sudo grep NC_ADMIN_PASSWORD .env # password for the "admin" account
Step 6: Install Caddy and enable HTTPS
Install Caddy from its official repository:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
Replace /etc/caddy/Caddyfile, which is not YAML, with the file below. The .well-known redirects come from the Nextcloud documentation and are needed for CalDAV and CardDAV:
sudo tee /etc/caddy/Caddyfile >/dev/null <<'EOF'
cloud.example.com {
redir /.well-known/carddav /remote.php/dav/ 301
redir /.well-known/caldav /remote.php/dav/ 301
header Strict-Transport-Security "max-age=15552000; includeSubDomains"
reverse_proxy 127.0.0.1:8080
}
EOF
sudo sed -i 's/cloud.example.com/YOUR-DOMAIN/' /etc/caddy/Caddyfile # adjust
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
Check: the curl command should return HTTP/2 200 and a JSON containing "installed":true. If it fails, Caddy’s logs show why the certificate was refused.
curl -sI https://cloud.example.com/status.php | head -n 1
curl -s https://cloud.example.com/status.php
sudo journalctl -u caddy --no-pager | tail -n 30
Step 7: Firewall, background jobs and reboot test
Enable UFW. Allow SSH from your local network only (adjust 192.168.1.0/24) before you enable it, or you’ll lock yourself out:
sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Note: ports published by Docker bypass UFW rules. That’s why Nextcloud is published on 127.0.0.1 only.
Nextcloud recommends running cron.php every 5 minutes. Add the job to root’s crontab (sudo crontab -e):
*/5 * * * * docker compose -f /opt/nextcloud/compose.yaml exec -T -u www-data app php -f /var/www/html/cron.php
Then:
sudo docker compose -f /opt/nextcloud/compose.yaml exec -u www-data app php occ background:cron
sudo reboot
Check after the reboot: the containers should be back up on their own, and the site should answer over HTTPS.
sudo docker compose -f /opt/nextcloud/compose.yaml ps
curl -s https://cloud.example.com/status.php
Verification checks
sudo docker compose ps(in/opt/nextcloud):dbandappshould both beUp. Unlikedocker ps -a, this command doesn’t mix in stopped containers from other projects.sudo ss -tlnp | grep -E ':(80|443|8080)\b': Caddy should be listening on 80 and 443, and 8080 only on127.0.0.1.- Open
https://cloud.example.com, log in asadmin, then go to Administration settings > Overview (account menu, top right). Nextcloud lists its security and setup warnings there. curl -sI https://cloud.example.com/.well-known/caldavshould return a301to/remote.php/dav/.
Security and best practices
- Expose only 80 and 443. No port forwarding for SSH, Samba or any admin interface. For remote access, use a VPN (WireGuard, for example).
- SSH: use key authentication (
ssh-copy-id), then setPasswordAuthentication noin/etc/ssh/sshd_config. Keep a session open while you test. - Docker socket: never mount it into an exposed container, and think twice before adding a user to the
dockergroup: it amounts to root access. - Nextcloud account: enable two-factor authentication (the “Two-Factor TOTP Provider” app) and create a non-admin account for everyday use.
- Updates:
sudo docker compose pull && sudo docker compose up -d. Nextcloud only upgrades one major version at a time. If you pin a version (nextcloud:NN), go up one number at a time. - Backups: put Nextcloud in maintenance mode, dump the database, then copy
/opt/nextcloudto another device:
cd /opt/nextcloud
sudo docker compose exec -u www-data app php occ maintenance:mode --on
sudo docker compose exec -T db sh -c 'mariadb-dump --single-transaction -u nextcloud -p"$MYSQL_PASSWORD" nextcloud' | sudo tee /root/nextcloud-db.sql >/dev/null
sudo docker compose exec -u www-data app php occ maintenance:mode --off
sudo head -n 5 /root/nextcloud-db.sql # checks that the dump isn't empty
Troubleshooting common errors
- “Access through untrusted domain”.
NEXTCLOUD_TRUSTED_DOMAINSis mainly applied at install time. To add the domain afterwards, runsudo docker compose exec -u www-data app php occ config:system:set trusted_domains 1 --value=cloud.example.com. - Caddy can’t get a certificate. Check your domain with
dig +short A, check that ports 80 and 443 are forwarded on the router and that you aren’t behind CGNAT, then readsudo journalctl -u caddy. - 502 Bad Gateway. Nextcloud isn’t answering on
127.0.0.1:8080. Checksudo docker compose logs appandsudo docker compose ps. - Redirect loop or
http://links. CheckOVERWRITEPROTOCOL=httpsandTRUSTED_PROXIES, thenocc config:list system. - Nextcloud is using SQLite. One of the four
MYSQL_*variables was missing on first start. Fix the file, then start again from an empty./nextcloudfolder. - Services stopped after a reboot. Check
systemctl is-enabled dockerand thatrestart: alwaysis set.
FAQ
Why Caddy rather than Nginx and Certbot?
Caddy gets and renews Let’s Encrypt certificates automatically, and its config is only a few lines long. Nginx with Certbot also works, but you don’t need it once Caddy is in place. Never run both on ports 80 and 443.
Can I use Debian instead of Ubuntu?
Yes. Use the current Debian stable release and the “Install Docker Engine on Debian” procedure from Docker’s documentation (it uses a different repository). Avoid Debian 11, whose LTS ends in 2026.
Can I turn it into a NAS?
Yes for file sharing through Nextcloud. You can add Samba for SMB on the local network, but never expose it to the Internet. A mini PC doesn’t have enough bays for multi-drive RAID.
What does it cost?
The software used here is free and open source. That leaves the domain name and electricity, which depend on your hardware and your provider.
Read also
- Monter un serveur ia locale gpu ollama
- Installer home assistant sur un mini pc
- Raspberry pi vs mini pc serveur
Host this yourself
No hardware to manage? This kind of service runs on a managed VPS, ready in minutes. See PlanetHoster plans