What you need before you start
A Docker home server is one Linux machine running your self-hosted apps as containers. It doesn't need to be special hardware. A used office mini PC or an old desktop is the most common starting point, because it is quiet, cheap to run, and has enough CPU for several apps at once. A Raspberry Pi 4 or 5 works too, as long as the apps you want publish arm64 images, which most popular ones do.
- 8 GB of RAM is comfortable for five to ten typical apps. 4 GB is enough for light services like Pi-hole, Vaultwarden, and Syncthing. Photo apps with machine learning, such as Immich, want more.
- An SSD for the operating system and app data. Databases on a spinning disk are slow. Media libraries can live on larger hard drives.
- Wired Ethernet. Wi-Fi works, but streaming and backups are the first things to suffer on it.
- A fixed address on your network. Set a DHCP reservation in your router so the server keeps the same IP. Every bookmark and app setting will point at it.
- Debian stable or the current Ubuntu Server LTS. Both are well supported by Docker and by every guide you will read later. Skip the desktop environment; you won't need it.
If you have a spare machine and are unsure, start with it. Moving a well-organised Docker setup to new hardware later is mostly copying folders, which is one of the main reasons to use Docker in the first place.
Install Docker and Docker Compose
Install Docker from Docker's own repository rather than the distribution package, so you get a current Engine and the Compose plugin together. Docker's convenience script does that in one step on Debian, Ubuntu, and Raspberry Pi OS:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.shThen let your user run Docker without sudo. Log out and back in afterwards so the new group applies.
sudo usermod -aG docker $USERMembership of the docker group is equivalent to root on that machine: anyone in it can start a container that mounts the whole filesystem. Only add accounts you would trust with sudo.
Check that both the Engine and Compose work:
docker run --rm hello-world
docker compose versionNote the space in docker compose. The old standalone docker-compose binary is deprecated, and guides that still use it translate one-to-one.
Give every app its own folder
The decision that saves you the most pain later is where things live. Keep one folder per app, holding that app's compose.yaml, its .env file, and its configuration. Keep bulk data such as media and photos somewhere separate.
/opt/stacks/
jellyfin/
compose.yaml
config/
immich/
compose.yaml
.env
postgres/
/srv/data/
media/
photos/
backups/With this layout, backing up the server means backing up two trees: /opt/stacks for everything that describes and configures your apps, and /srv/data for the files they serve. Rebuilding on a new machine means installing Docker, copying both trees back, and running docker compose up -d in each folder.
That is also why this guide uses bind mounts (a host folder such as ./config mapped into the container) rather than named volumes. Named volumes work, but they live under /var/lib/docker/volumes, where they are easy to forget when you set up backups.
sudo mkdir -p /opt/stacks /srv/data/media /srv/data/photos /srv/data/backups
sudo chown -R $USER:$USER /opt/stacks /srv/dataRun your first Compose stack
Jellyfin makes a good first app: it is useful on day one, it shows how ports, volumes, and hardware access work, and it runs happily on modest hardware. Create /opt/stacks/jellyfin/compose.yaml:
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
ports:
- "8096:8096"
volumes:
- ./config:/config
- ./cache:/cache
- /srv/data/media:/media:ro
devices:
- /dev/dri:/dev/dri # Intel or AMD GPU; remove if the machine has none
restart: unless-stoppedportsmaps port 8096 on the server to port 8096 in the container. The left side is the host, the right side is the container.volumeskeeps Jellyfin's configuration in the app folder, and mounts your media read-only (:ro), so a misbehaving app can't delete your library.devicespasses the GPU through for hardware transcoding, which is what lets a small mini PC stream to several screens at once.restart: unless-stoppedbrings the container back after a crash or a reboot, but respects it when you stop it on purpose.
Start it and watch the logs until it settles:
cd /opt/stacks/jellyfin
docker compose up -d
docker compose logs -fPress Ctrl+C to leave the logs; the container keeps running. Open http://your-server-ip:8096 and Jellyfin's setup wizard appears. Every other app follows the same pattern: a folder, a compose file, docker compose up -d.
About :latest: it is fine for self-contained apps like Jellyfin. For databases, pin the major version (for example postgres:16), because a major database upgrade needs a migration and should never happen by accident during a routine pull.
Ports, networks, and what not to expose
Compose creates a private network for each stack, and containers on it reach each other by service name. When an app's compose file has a db service, the app connects to db:5432, and that database never needs a port on the host at all. Only publish the port you actually open in a browser.
One trap catches almost everyone: ports published by Docker bypass UFW. Docker writes its own firewall rules, so a ufw deny 5432 does not protect a database you published with 5432:5432. If a service must be reachable from the host but not from your network, bind it to localhost instead:
ports:
- "127.0.0.1:5432:5432"Two more rules keep a first server safe:
- Don't forward ports on your router to reach apps from outside. Use a mesh VPN such as Tailscale for your own devices, or a tunnel such as Cloudflare Tunnel for the one app you want to share publicly.
- Once you have more than a handful of apps, put a reverse proxy (Caddy, Nginx Proxy Manager, or Traefik) in front of them, so you get names like
jellyfin.home.lanand HTTPS instead of a list of port numbers.
Keep passwords out of the compose file
Compose reads a .env file in the same folder automatically and substitutes its values into compose.yaml. Put database passwords and API keys there, so the compose file itself can be shared, pasted into a forum post, or kept in Git without leaking anything.
DB_PASSWORD=change-me-to-something-long
UPLOAD_LOCATION=/srv/data/photos environment:
DB_PASSWORD: ${DB_PASSWORD}Restrict who can read it with chmod 600 .env, and add .env to .gitignore if the stacks folder is a Git repository.
Update apps on purpose
Updating a Compose app is two commands: pull the new image, then recreate the container from it. Your data survives, because it lives in the mounted folders, not in the container.
cd /opt/stacks/jellyfin
docker compose pull
docker compose up -d
docker image prune -fThe last command deletes the old, now-unused images; skip it and they slowly fill the disk. Whether to automate updates depends on the app. Self-contained apps are safe to update on a weekly schedule. Apps with a database and a fast release cycle, Immich being the usual example, occasionally ship changes that need a manual step, so read their release notes before pulling.
Back up before you trust it with anything
The most common way to lose data on a home server is to set up backups after the first disk failure. Decide what you are protecting before you import the family photos:
/opt/stacks: compose files,.envfiles, and app configuration. Small, and the thing that takes longest to recreate by hand.- Databases: dump them rather than copying their data folder while they run. A copied live Postgres folder can restore as a corrupt one.
/srv/data: photos, documents, and media. Large, but often the only copy that exists.
cd /opt/stacks/immich
docker compose exec -T database pg_dumpall -U postgres > /srv/data/backups/immich-$(date +%F).sqlThe service is called database in Immich's compose file; use whatever name your app's file gives it. Then copy everything to a second place with a tool built for it, such as restic, Borg, or Kopia, all of which do deduplicated, encrypted backups and can send them off-site. Follow the 3-2-1 rule: three copies, on two kinds of storage, one of them outside your house. Finally, restore one file on purpose. A backup you have never restored is a hope, not a backup.
