Tech Digest

Start here: from nothing to a server you trust

Six stages, in order. Each one ends with something working, and stage 5 comes before the fun part on purpose.

Last reviewed

How do you start self-hosting?

In six stages, in this order: decide whether it is worth it for you, get a machine (an old laptop is a legitimate first answer), install Debian stable plus Docker with one directory tree under /srv, run one small service you use daily, reach it over HTTPS by name and then from outside without a port forward, and set up a backup you have actually restored from. Only then add more services. The whole path is about five hours of hands-on work and costs 40 to 100 USD a year if you already own the hardware.

Six stages, in order. Each one ends with something that runs, so each one is also a stopping point. If you reach the end of stage 5 and never do stage 6, you have a working server and a backup you have tested, which is more than a lot of people have after two years of collecting containers.

The order is the advice. Almost every self-hosting disaster is a stage done out of sequence.

Stage 0: decide whether to do this at all#

The cost of self-hosting is attention, not money. Steady state is one to two hours a month of updates, plus one evening a year when something breaks in a way that is not obvious. Three questions decide whether that is a good trade for you:

Who fixes it when it breaks and you are not home? If the honest answer is nobody, that is fine, as long as nothing load-bearing for other people lives on the box. Household DNS, the calendar your partner runs their week on and the only copy of the family photos are not good candidates for a machine with a bus factor of one.

Are you doing this to save money on one subscription? You will not. A new mini PC, a domain and off-site backup storage is roughly 250 to 350 USD in year one against maybe 60 USD of subscription. Self-hosting wins on price when you are replacing several services at once, or storing hundreds of gigabytes, or when the actual point is that nobody else can change the terms.

Is the data irreplaceable and currently uncopied? Then your first project is a backup of it, in the cloud service you already use, today. Migrating it onto a server you are still learning to run is how people lose it.

The uncomfortable part: from the day you put something useful on this machine, you are the on-call engineer for your household, and nobody voted for that. When not to self-host is the longer version, including the four categories of thing you should keep renting.

Stage 1: the machine, and the old laptop counts#

The laptop in your cupboard is a legitimate first server. It has a CPU from the last decade, 8 GB of RAM, and a battery that behaves like a small uninterruptible power supply, which is more than a bare mini PC gives you. Two fixes on day one:

bash
# stop it suspending when you close the lid
sudo sed -i 's/^#\?HandleLidSwitch=.*/HandleLidSwitch=ignore/' /etc/systemd/logind.conf
sudo sed -i 's/^#\?HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=ignore/' /etc/systemd/logind.conf
sudo systemctl restart systemd-logind

Then check the disk: a 2014 spinning drive with 40,000 powered-on hours is the component most likely to end your project. Expect 15 to 25 W idle from a laptop against 7 to 12 W from a current mini PC.

If you are buying: an Intel N150 mini PC with 16 GB of DDR5 and a 500 GB NVMe drive, roughly 180 to 250 USD, idling under 10 W. That runs a reverse proxy, a DNS blocker, a password manager, a wiki, a feed reader and a media server doing direct play, simultaneously, without effort. What it will not do is hold a photo library with machine learning on top: Immich idles around 900 MB across four containers and wants 8 GB to itself.

Buy for the stack you will run in the next six months, because RAM and disks are the only upgrades anyone performs. And compare idle watts, not thermal design power: over five years, 8 W against 60 W is about 64 USD of electricity against about 482 USD. Choosing home server hardware has three costed builds, Mini PC vs NAS vs used enterprise settles the form factor, and Stack planner turns your service list into a RAM figure before you spend anything.

Stage 2: the base, which is boring on purpose#

Debian stable, on bare metal, with Docker from Docker's own apt repository. Not a hypervisor, not a NAS appliance, not a desktop edition.

bash
sudo apt-get update && sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/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/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Use Docker's repository rather than Debian's docker.io package, which runs several major versions behind. Then two pieces of housekeeping before anything else exists.

bash
# one tree, and you own the path
sudo mkdir -p /srv/stacks /srv/data
sudo docker network create edge
json
// /etc/docker/daemon.json  then: sudo systemctl restart docker
{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

Docker's default json-file log driver has no size limit at all. One chatty container will fill your root filesystem, and it always happens on a Sunday.

The directory tree is the decision that is expensive to reverse. /srv/stacks/<service>/compose.yaml for definitions, /srv/data/<service>/ for every bind mount, and nothing anywhere else. That makes "back up my server" a single path, and it makes moving to a bigger machine a rsync plus a docker compose up -d. Named volumes are the alternative: real storage in /var/lib/docker/volumes/<name>/_data that you cannot see in a file manager and that a stray docker volume prune deletes without asking. Docker Compose conventions covers ownership, PUID/PGID and the rest of the layout.

Two honest notes about Docker Engine. Anyone in the docker group is effectively root on the host, so usermod -aG docker $USER is a real privilege grant, not a convenience. And published ports bypass ufw: a container run with -p 8080:80 is reachable even when the firewall says that port is denied, because Docker writes its own iptables chains. Bind to a specific interface (-p 127.0.0.1:8080:80) and let a proxy do the exposing.

Stage 3: one service you will notice breaking#

Pick something with one container, a data surface you can read with ls, and daily use. linkding is the reference choice here: a Django bookmark manager on SQLite, 90 MB idle, operational load 1 out of 5, browser extensions that work, and a documented backup command. You will use it within an hour of installing it, which means you will notice within a day if it breaks. That feedback is the entire reason to have a first service.

yaml
# /srv/stacks/linkding/compose.yaml
name: linkding

services:
  linkding:
    image: sissbruecker/linkding:latest
    container_name: linkding
    restart: unless-stopped
    environment:
      LD_SUPERUSER_NAME: "you"
      LD_SUPERUSER_PASSWORD: "change-me-before-first-start"
      LD_CSRF_TRUSTED_ORIGINS: "https://links.home.example.com"
      TZ: "Europe/Berlin"
    volumes:
      - /srv/data/linkding:/etc/linkding/data
    ports:
      - "127.0.0.1:9090:9090"
    networks:
      - edge

networks:
  edge:
    external: true
bash
cd /srv/stacks/linkding && docker compose up -d
curl -sI http://127.0.0.1:9090/ | head -1     # expect HTTP/1.1 200 OK

Three deliberate choices in that file. The published port is bound to 127.0.0.1, so the only way in from another machine is through the proxy you build next. The edge network is external and shared, so the proxy can reach the container by name without publishing anything. And the image tag is latest rather than latest-plus: the -plus image bundles Chromium for local page snapshots and the documentation is explicit that it needs substantially more memory.

If bookmarks are not your thing, Navidrome (70 MB, port 4533, reads your music read-only and never writes tags) or Vikunja (70 MB, tasks with CalDAV) fit the same shape. Do not start with Pi-hole: DNS for the whole house is load-bearing for people who did not consent to your hobby.

Stage 4: reach it, local name first#

Do this in two steps, because the local step teaches you the proxy and the remote step teaches you the network, and debugging both at once is miserable.

Local. Install avahi-daemon and the box answers to <hostname>.local on your LAN with no DNS configuration. Better, if your router allows static DNS entries, point home.example.com at the server's LAN address. Then put Caddy in front, including for services that will never be public.

yaml
# /srv/stacks/caddy/compose.yaml
name: caddy

services:
  caddy:
    image: caddy:2
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - /srv/stacks/caddy/conf:/etc/caddy
      - /srv/data/caddy:/data
      - /srv/data/caddy-config:/config
    networks:
      - edge

networks:
  edge:
    external: true
caddyfile
# /srv/stacks/caddy/conf/Caddyfile
links.home.example.com {
    tls internal
    reverse_proxy linkding:9090
}

Mount the directory at /etc/caddy, not the Caddyfile itself: editors replace the file's inode on save and the container keeps reading the old one. tls internal makes Caddy mint its own certificate authority, whose root lands at /srv/data/caddy/caddy/pki/authorities/local/root.crt. Install that on each device once and internal HTTPS stops throwing warnings. Keep /srv/data/caddy persistent, or every redeploy throws away the ACME account and re-requests certificates you are rate limited on.

For real public certificates without exposing the server, you want the DNS-01 challenge, and the stock Caddy image ships zero DNS provider modules, so that means an xcaddy build you rebuild on every version bump. Reverse proxy and TLS walks through that decision, and Reverse proxy generator writes the config for your service list.

Remote. Not a port forward. Install a WireGuard-based mesh and your phone gets an address on the server's network from anywhere, with no inbound port open and your home IP absent from anyone's scan results. Headscale plus the official Tailscale clients is the self-hosted end of that; wg-easy is plain WireGuard with a UI if you prefer one moving part. Remote access without port forwarding compares them, including what to do behind CGNAT, where port forwarding is not available to you at all.

Stage 5: back it up before you add anything else#

This stage comes before the fun part, and it is the one people skip. The reason is arithmetic: within a year a working server accumulates documents, scans, photos and configuration that exists nowhere else, and the moment that happens without a tested restore, you are running a data loss experiment. 81.9% of the 105 tools profiled here cannot be safely backed up by copying their files while they run. linkding is one of them: its own documentation says copying db.sqlite3 is not transaction safe and may give you a corrupted database.

So the backup has two halves. Dump what needs dumping, then snapshot the tree.

bash
#!/usr/bin/env bash
# /usr/local/sbin/backup.sh   (root, chmod 700)
set -euo pipefail

# 1. app-consistent dumps first
docker exec linkding python manage.py full_backup /etc/linkding/data/backup.zip

# 2. then one snapshot of everything
export RESTIC_REPOSITORY="s3:s3.us-west-004.backblazeb2.com/YOURBUCKET/srv"
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic backup /srv --tag srv --exclude-caches
restic forget --tag srv --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
restic check --read-data-subset=1/30
curl -fsS -m 10 https://hc-ping.com/YOUR-UUID      # dead man's switch
ini
# /etc/systemd/system/backup.timer     systemctl enable --now backup.timer
[Unit]
Description=Nightly restic backup of /srv
[Timer]
OnCalendar=*-*-* 03:30
Persistent=true
[Install]
WantedBy=timers.target

Four details that are not decoration. forget without --tag applies retention to every snapshot in the repository, which is how people delete another machine's history. forget alone frees no space; prune does. check without --read-data never reads pack contents, so it cannot see bit rot, and --read-data-subset=1/30 covers the whole repository across a month. And the final curl is a dead man's switch, because a backup job that silently stops running is the normal failure, not a corrupt archive.

Then do the thing that makes it a backup instead of a hypothesis:

bash
restic restore latest --target /tmp/restore-test --include /srv/data/linkding
diff -r /tmp/restore-test/srv/data/linkding /srv/data/linkding && echo RESTORE OK

Finally, print the restic passphrase and put it somewhere physical. It is the only key, there is no vendor and no recovery path, and if it lives only in an environment file on the machine you are backing up, losing that machine loses the backups too. Backups that actually restore covers retention, off-site copies and rehearsals; Backup planner sizes the storage for your stack.

Stage 6: now add things#

Now the fun part is safe, because a bad upgrade costs you an hour instead of a year.

Add services one at a time, and read the operational load score before the feature list. Across the index, 60.0% of tools sit at 1 or 2 out of 5 and 61 of 105 are a single container. Those you can add on a weeknight. The 4s and 5s are projects.

ToolCategoryOps loadIdle RAM
DozzleMonitoring1, Set and forget15 MB
LLDAPIdentity1, Set and forget15 MB
File BrowserPhotos and files1, Set and forget20 MB
GotifyCommunication1, Set and forget20 MB
CaddyNetworking1, Set and forget25 MB
DiunSecurity1, Set and forget25 MB
GatusMonitoring1, Set and forget25 MB
GlanceDashboards1, Set and forget25 MB
ntfyCommunication1, Set and forget30 MB
Pocket IDIdentity1, Set and forget30 MB
BeszelMonitoring1, Set and forget40 MB
grocyProductivity1, Set and forget40 MB

Browse the full directory at /apps/, or pick a category: Backup, Monitoring, Media, Documents, Identity. Then run the shortlist through Stack planner, which adds up idle RAM and operational load so you find out before you install that your 16 GB box cannot hold Immich, Frigate and authentik at once.

A good second and third service: Uptime Kuma (120 MB, one container) so you find out about breakage from a notification rather than from a family member, and Beszel (40 MB) for CPU, memory and disk across machines. Both are cheap and both make the rest of the stack safer.

The mistakes that cost the most#

  • Adding the second service before the first backup. Everything else on this list is recoverable. This one is how people lose a year of photos in month eleven.
  • Storing data in named volumes you cannot name. docker compose down -v and docker volume prune both delete real data instantly, and neither asks twice.
  • Forwarding port 443 to an application's login page. Application login forms are written by application developers, rarely rate limit, and one pre-auth bug is a foothold on your LAN. Proxy first, auth in front, or use a mesh VPN.
  • Assuming ufw covers Docker. It does not. Published ports go around it.
  • Automatic updates on stateful applications. Watchtower is fine pointed at Caddy and dreadful pointed at anything with a schema. Immich does not support downgrades at all, Uptime Kuma's v1 to v2 migration is one way, Nextcloud cannot skip a major version, and wg-easy shipped no v14 to v15 upgrade path whatsoever. See An update strategy that does not lose data.
  • Making DNS load-bearing on day one. A failed Pi-hole upgrade means nobody in the house can reach anything, including the search engine you need to fix it.
  • Keeping the backup passphrase only on the machine being backed up.
  • Buying used enterprise gear for a light stack. 60 to 90 W of idle draw costs more per year than the mini PC that would have done the job.

What this actually costs#

StageHands-on time
0, decide20 minutes of reading
1, machine0 if you own it, otherwise a week of shipping
2, Debian and Docker45 minutes
3, first service15 minutes
4, proxy and remote access60 to 120 minutes, mostly DNS
5, backups and one restore test90 minutes

Four to six hours, comfortably a weekend. In money, per year, assuming you already have a machine: 10 to 15 USD for a domain, 12 to 60 USD for around 100 GB of off-site backup storage, and about 16 USD of electricity for a 10 W box (87.6 kWh at the US average of 18.34 cents per kWh, or roughly 26 EUR at 0.30 EUR per kWh). That is 40 to 100 USD a year. A new N150 mini PC adds 180 to 250 USD once. What a home server costs to run does the full arithmetic including disks.

You are done when#

  • docker ps shows every container Up, not Restarting.
  • You reach your service over HTTPS by name, with no certificate warning, from a device that is not the server.
  • You reach it from outside the house without a single forwarded port.
  • restic snapshots lists a snapshot from last night that you did not trigger by hand.
  • You have restored a real file from that snapshot and diffed it against the original.
  • The backup passphrase exists somewhere that is not the server. Paper counts.
  • Something alerts you when the backup stops running.
  • /etc/docker/daemon.json caps log size, and docker system df is a number you have looked at once.
  • Asked where the data for any service lives, you answer with a path in one second.

Tick all nine and you have a server, not a science project.

What to do next#

If you are still at stage 0, read When not to self-host before you buy anything. If you are past stage 5, A security baseline for a home server and The minimum viable monitoring stack are the two follow-ups that pay for themselves, and Self-hosting FAQ answers the questions that come up in month two: CGNAT, family accounts, abandoned projects and what never to self-host.

Questions#

How long does it take to set up a first home server?

About four to six hours of hands-on time, realistically spread over a weekend. Installing Debian and Docker is 45 minutes, the first service is 15, and the two stages people underestimate are remote access (60 to 120 minutes, most of it DNS) and backups (90 minutes if you include one real restore test, which you should). After that, steady state is one to two hours a month of updates and attention, plus one genuinely bad evening a year.

Can I use an old laptop as a home server?

Yes, and it has two advantages a mini PC does not: a battery that acts as a small UPS, and you already own it. Two things to fix on day one. Set HandleLidSwitch=ignore and HandleLidSwitchExternalPower=ignore in /etc/systemd/logind.conf or it suspends when you close it, and expect 15 to 25 W idle rather than the 7 to 12 W of a modern N150 mini PC. If the disk is a spinning drive from 2014, replace it before you put data on it.

What should I self-host first?

Something with one container, a data directory you can read with ls, and a use often enough that you notice within a day when it breaks. linkding (one container, 90 MB idle, port 9090, one SQLite file) and Navidrome (one container, 70 MB, port 4533, never writes to your music files) both qualify. The point of the first service is to make you run the full loop once: deploy, proxy, back up, restore. Photos and passwords are the wrong first service, because the cost of getting the loop wrong is permanent.

Do I need a domain name to start?

Not for stage 3, and yes by stage 4 if you want certificates browsers trust. A domain with DNS at a provider that has an API costs 10 to 15 USD a year and unlocks the DNS-01 challenge, which is how you get real certificates for services that are not reachable from the internet at all. The free path is Caddy with tls internal, which mints a local certificate authority you then install on every device by hand. That is fine for two devices and tedious for six.

How much does a first server cost per year?

If you already own the machine: 10 to 15 USD for a domain, 12 to 60 USD for off-site backup storage of around 100 GB, and about 16 USD of electricity for a 10 W box at the US average of 18.34 cents per kWh (87.6 kWh a year). Call it 40 to 100 USD. Buying a new N150 mini PC with 16 GB of RAM adds roughly 180 to 250 USD once. Used enterprise gear at 60 to 90 W costs more in electricity every year than the machine cost to buy.

Should I learn Docker first or install applications directly?

Use Docker Compose. Installing directly means you own the dependency conflicts: two applications wanting different PHP or Python versions on the same box is a normal Tuesday, and there is no clean uninstall. With Compose, the definition of a service is a text file you can read, version, move to another machine and delete. Docker Engine costs about 120 MB of idle RAM for dockerd plus containerd and scores 2 out of 5 on operational load, which is the cheapest insurance on this page.

Do I need to open ports on my router?

No, and you should not start there. A WireGuard-based mesh (Tailscale, or Headscale if you want to run the control server yourself) gives your phone an address on the server's network with zero inbound ports open, which also means your home IP never appears in a scanner's results. Port forwarding only becomes worth considering when you have something genuinely public, and even then the forwarded port goes to a reverse proxy, never to an application's own login page.

When should I add a second service?

After restic snapshots shows a snapshot you did not run by hand, and after you have restored a real file from it. That is the entire gate. The failure mode this prevents is common and specific: people spend three months adding services, accumulate 200 GB of photos and documents, then discover their backup has been silently failing since week two, or that copying a live SQLite file gave them a corrupt database. 81.9% of the 105 tools profiled here cannot be safely backed up by a live file copy.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.