Tutorials 10 min read

Recycle an Old PC into a Home Server in 2026

Turn an old PC into a home server in 2026: Ubuntu Server LTS, official Docker, Nextcloud with MariaDB and Caddy for automatic HTTPS, plus a firewall and backups.

See our picks
S By Selfhostr Team · no sponsored content
Recycle an Old PC into a Home Server in 2026
ⓘ ⓘ This article may contain affiliate links (no extra cost to you, it funds the site). As an Amazon Associate, Selfhostr earns from qualifying purchases. See the disclosure.
⏱️
~45 min
Duration
🎚️
Intermediate
Difficulty
💶
0 € (free software)
Cost
📦
Docker installed and configured
Key Prerequisites

👍 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 you
Recommended
📦

Lenovo ThinkCentre M720q Tiny

Voir sur Amazon

Amazon · price and availability on the listing

Alternative
📦

HP ProDesk 600 G5 Mini

Voir sur Amazon

Amazon · price and availability on the listing

See all our recommended gear in the shop.

📑 Contents ▾

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:

  1. Pick the target disk. ⚠️ “Use an entire disk” wipes that disk. Back up anything you need first, and check the model and size shown.
  2. Create your user. It is not root: every admin command will go through sudo.
  3. 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

  1. 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
  1. At your registrar, create an A record for cloud.example.com that points to your router’s public IPv4 address (shown in its interface).
  2. 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): db and app should both be Up. Unlike docker 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 on 127.0.0.1.
  • Open https://cloud.example.com, log in as admin, 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/caldav should return a 301 to /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 set PasswordAuthentication no in /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 docker group: 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/nextcloud to 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_DOMAINS is mainly applied at install time. To add the domain afterwards, run sudo 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 read sudo journalctl -u caddy.
  • 502 Bad Gateway. Nextcloud isn’t answering on 127.0.0.1:8080. Check sudo docker compose logs app and sudo docker compose ps.
  • Redirect loop or http:// links. Check OVERWRITEPROTOCOL=https and TRUSTED_PROXIES, then occ 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 ./nextcloud folder.
  • Services stopped after a reboot. Check systemctl is-enabled docker and that restart: always is 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

Host this yourself

No hardware to manage? This kind of service runs on a managed VPS, ready in minutes. See PlanetHoster plans

Tags: homelabrecyclingnasservercloud

Related

Tutorials

Getting Started with Docker Compose in Your Home Lab for 2026

First Docker Compose stack for your homelab: Docker from the official repository, the Compose v2 plugin, Nginx, MariaDB and LAN-only phpMyAdmin, with valid files and a check at every step.

Read
Tutorials

Metal Rack Assembly for Home Lab 2026: Step-by-Step Guide

Discover our comprehensive and detailed tutorial to install a metal rack in 2026. Follow our practical tips, complete configuration files, and exact commands.

Read
Tutorials

Create a Raspberry Pi 5 NAS in 2026: Complete Guide

Guide for 2026 to transform a Raspberry Pi 5 into a high-performance NAS. Step-by-step guide on case, NVMe SSD, Docker, Samba setup and security for reliable self-hosting.

Read