Skip to content

Storage & Docker on Linux

The Linux equivalent of NAS base setup. Paths used on this page:

Synology Old PC (this page) On
/volume1/data /mnt/storage/data data HDD(s)
/volume1/docker /opt/docker OS SSD (fast for app databases)

1. Mount the data disks

Find the disks and create a filesystem (this erases them):

lsblk -o NAME,SIZE,MODEL,SERIAL,FSTYPE,MOUNTPOINT
sudo mkfs.ext4 -L data1 /dev/sdX          # or: sudo mkfs.xfs -L data1 /dev/sdX  (apt install xfsprogs)
sudo blkid /dev/sdX                       # copy the UUID

Mount it by UUID in /etc/fstab:

UUID=<DISK_UUID>  /mnt/disk1  ext4  defaults,noatime,nofail  0  2
sudo mkdir -p /mnt/disk1 && sudo systemctl daemon-reload && sudo mount -a && df -h /mnt/disk1

nofail lets the box boot (and you SSH in) even if a disk dies. ext4 and XFS are both fine for media; pick one.

Pick a layout for more than one disk

Mount it straight at /mnt/storage. Simple. No redundancy, so the restic backups and your ability to re-download are the safety net.

Each disk keeps its own ext4/XFS filesystem (/mnt/disk1, /mnt/disk2, …); mergerfs pools them into /mnt/storage, and SnapRAID adds scheduled parity on one extra disk at least as large as the biggest data disk.

sudo apt install mergerfs snapraid
/etc/fstab (pool line)
/mnt/disk*  /mnt/storage  fuse.mergerfs  cache.files=off,category.create=epmfs,moveonenospc=true,dropcacheonclose=true,minfreespace=50G,fsname=mergerfs,nofail  0  0
  • Good: mix any sizes, add disks one at a time, a dead disk only loses its own files.
  • Catch: parity is only as fresh as the last snapraid sync (run it from a systemd timer).
  • Hardlinks only work within one disk. epmfs (existing-path) puts new files on a disk where the parent folder already exists, so a download and its hardlink stay together. Create the data/ tree on each disk, and test with ls -li. See the mergerfs docs.
  • The Debian/Ubuntu mergerfs package can be old; the project publishes current .deb files on its GitHub releases.
sudo apt install zfsutils-linux          # Debian: enable contrib and install zfs-dkms + zfsutils-linux
sudo zpool create -o ashift=12 -O compression=lz4 -O atime=off -m /mnt/storage tank mirror /dev/disk/by-id/<DISK_A> /dev/disk/by-id/<DISK_B>
  • Good: real-time redundancy, checksums/self-healing, cheap snapshots (closest to Btrfs + Snapshot Replication on DSM).
  • Catch: drives should be the same size, growing means adding another mirror pair, and it likes RAM. Don't put ZFS in /etc/fstab; ZFS mounts the pool itself.

2. Users and PUID/PGID

sudo groupadd -g 1000 media 2>/dev/null || true     # or reuse an existing group; note its GID
sudo useradd -r -M -s /usr/sbin/nologin -g media <media_user>
sudo useradd -r -M -s /usr/sbin/nologin -g media <plex_user>
id <media_user>; id <plex_user>                      # → ${PUID}, ${PLEX_PUID}, ${PGID}

On Synology the shared group is users (100). Here use the group you created; set PGID to its GID in every stack. Keep UMASK=002.

3. The /data folder structure (same as the NAS)

sudo mkdir -p /mnt/storage/data/{torrents/{incomplete,movies,tv,watch},usenet/{incomplete,complete/{movies,tv}},media/{movies,tv}}
sudo chown -R <media_user>:media /mnt/storage/data
sudo chmod -R a=,a+rX,u+w,g+w /mnt/storage/data     # 775 dirs / 664 files
sudo mkdir -p /opt/docker && sudo chown <media_user>:media /opt/docker

torrents and media must be on the same filesystem for hardlinks and instant moves. See TRaSH's Hardlinks and Instant Moves.

4. Docker Engine + compose plugin

Use Docker's own repository, not docker.io from the distro or the snap. From the official Ubuntu / Debian install pages:

sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc | cut -f1)
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 <<SRC
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
SRC
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-doc podman-docker containerd runc | cut -f1)
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/debian/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 <<SRC
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
SRC
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo docker run --rm hello-world
docker compose version           # v2 plugin: `docker compose`, not `docker-compose`

Optional: sudo usermod -aG docker $USER (log out and in). Members of docker are effectively root.

5. Portainer

Same as Portainer, with the Linux config path:

sudo docker run -d --name portainer-ce --restart unless-stopped \
  -p 9443:9443 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /opt/docker/portainer-ce:/data \
  portainer/portainer-ce:lts

Open https://<SERVER_IP>:9443 within a few minutes (the first-run setup times out) and create the admin user.

6. Plex hardware transcoding (Quick Sync)

Add the iGPU to the Plex stack:

    devices:
      - /dev/dri:/dev/dri

The linuxserver image gives its internal user access to /dev/dri automatically. Check the host side:

ls -l /dev/dri/renderD128         # group is usually "render"
getent group render               # if Plex can't open the device: add `group_add: ["<render_GID>"]` to the service

Then in Plex (Plex Pass required): Settings → Transcoder → Use hardware acceleration when available ON, Use hardware-accelerated video encoding ON, Enable HDR tone mapping ON. Play something with Convert automatically to a lower quality and look for (hw) in the Dashboard. Details: Plex's hardware-accelerated streaming article.

Note

With working Quick Sync you can relax the Direct-Play-only tuning on the Plex page (for example allow more than 1 transcode). The client-side "Original quality" advice still saves quality and bandwidth.

7. Firewall (ufw)

sudo apt install ufw
sudo ufw default deny incoming && sudo ufw default allow outgoing
sudo ufw allow from ${LAN_SUBNET} to any port 22 proto tcp     # SSH, LAN only
sudo ufw allow 32400/tcp                                       # Plex (host network; remote access)
sudo ufw enable && sudo ufw status verbose

Docker bypasses ufw for published ports

Ports published with ports: (Portainer 9443, Sonarr, Radarr, Seerr…) are opened by Docker's own iptables rules and ignore ufw. Keep them off the internet by not forwarding them on the router, or bind them to the LAN address ("<SERVER_IP>:9443:9443"). Containers on network_mode: host (Plex) are filtered by ufw. See Docker's Docker and ufw.

8. Synology → Linux mapping

Every stack page works unchanged apart from these substitutions:

On the Synology pages On the old PC
/volume1/data (share data) /mnt/storage/data
/volume1/docker (share docker) /opt/docker
DSM users, group users (100) Linux users, your media group's GID
Container Manager, /usr/local/bin/docker, old docker-compose v1 Docker Engine from docker.com, docker compose v2
DSM Task Scheduler (scripts, for example a qBittorrent alt-speed toggle if you script it instead of using qBittorrent's built-in scheduler) systemd timer (or root crontab -e)
Snapshot Replication (#snapshot folders) ZFS snapshots (zfs snapshot, or sanoid), Btrfs snapshots (snapper) if you chose Btrfs, or skip on ext4/XFS and rely on restic
Hyper Backup N/A, use restic → B2 as in the guide
DSM firewall ufw (above)
DSM storage/SMART alerts smartmontools (smartd), zed for ZFS
SHR / Storage Manager → Expand mergerfs (+SnapRAID) or a ZFS mirror (above)
systemd timer instead of a DSM scheduled task
/etc/systemd/system/media-task.service
[Unit]
Description=Media stack scheduled task

[Service]
Type=oneshot
ExecStart=/opt/docker/scripts/<script>.sh
/etc/systemd/system/media-task.timer
[Unit]
Description=Run media-task on a schedule

[Timer]
OnCalendar=*-*-* <HH>:<MM>:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload && sudo systemctl enable --now media-task.timer
systemctl list-timers media-task.timer

Then carry on with the build order from step 3 (restore, then deploy the stacks).