← Blog

I Just Wanted a NAS: How a Weekend Storage Project Turned Into a Whole Homelab

August 2, 2026

It's a Saturday afternoon and I get a notification from my credit card company, it's Google Services charging me $1.99 to have an extra 100 GB of storage throughout the Google family of products across Drive, Photos, and more. I'm relatively particular about the types of services I subscribe to and felt like this was a subscription that I could get rid of with a NAS.

There are two main options when considering the route, build or buy? There are several products on the market like Synology, UGREEN, QNAP, and more. They provide both the hardware and Operating System for you to store your data. It's an incredibly streamlined experience for users to go from nothing to having stood up a highly available storage solution without the headache of configuring your own system.

That said, I had a lot of time on my hands that weekend and had some spare parts from building and selling PCs while I was in high school. I figured this could be a great segue into my fantasy of having my own server at home running background jobs and hosting services for my own workflows. Ultimately it comes down to how hands-on you'd be willing to get. For me this was an opportunity to get some experience in systems engineering and also actually do something about the pile of hardware my missus had been nagging me about in our garage.

The operating system

At the most barebones level, I could connect a large hard drive to the router and call it a day. It would be accessible on the local network and meet the basic requirements. However, we'd lose all the niceties that a dedicated machine provides through its operating system, such as redundancy, snapshots, data-integrity checks, and better performance. My requirements for the operating system were the following:

  1. Easily deploy containers for my services and workflows.
  2. Host game servers.
  3. Host GPU accelerated LLM models.

While exploring free operating systems, I boiled it down to two main options: TrueNAS and Proxmox.

TrueNAS is a storage operating system first. Everything about it is geared toward managing disks and sharing files: creating pools, setting up SMB and NFS shares, managing users and permissions, scheduling snapshots, and replicating data to another machine. It all happens through a polished web UI. TrueNAS can run apps and virtual machines too, but they're an add-on to the storage rather than the main event.

Proxmox flips that around. It's a virtualization platform first: its job is to run virtual machines and lightweight Linux containers, each isolated from the others. It has excellent ZFS support built in, but it has no interface for file sharing. If I want Proxmox to act as a NAS, I have to set that part up myself, either by running TrueNAS inside a VM or by sharing the storage from a small container running Samba.

Ultimately, I chose Proxmox because my requirements were about running services, not just storing files. Lightweight containers are built in, so deploying a new service or workflow takes minutes, and anything packaged for Docker can run inside a dedicated VM or container. Game servers each get their own isolated space with their own CPU and memory limits, so one misbehaving server can't drag down everything else, and I can snapshot it before trying out mods or updates. Most importantly, Proxmox lets me pass my GPU through to a VM or share it with containers, which makes hosting GPU-accelerated LLMs straightforward. TrueNAS could have covered some of this, but on Proxmox these workloads are the main event rather than an add-on. The trade-off is setting up file sharing myself, and for a machine that needs to do all of this at once, that was a price worth paying.

Configuring the NAS

The NAS is built in two layers: ZFS stores the data, and Samba shares it over the network. The storage lives on the Proxmox host, and the file sharing runs in its own lightweight container.

Storage: a ZFS mirror

I mirrored two 1.92 TB NVMe SSDs into a ZFS pool called tank. A mirror keeps an identical copy of everything on both drives, so either one can fail without losing data. The trade-off is that I only get one drive's worth of capacity, about 1.68 TB usable.

zpool create -o ashift=12 \
  -O compression=lz4 -O atime=off \
  -O xattr=sa -O acltype=posixacl \
  tank mirror /dev/disk/by-id/<drive-1> /dev/disk/by-id/<drive-2>

I then split the pool into three datasets, which work like folders with their own settings and snapshots: tank/data for files shared over the network, tank/vms for VM and container disks, and tank/backups for backups. I also added tank/vms as a storage target in Proxmox, so new VMs and containers live on the mirror too.

The NAS container

Instead of running a full NAS operating system in a VM, I created a small Debian 12 container called nas. It needs only 512 MB of RAM and an 8 GB disk. I then bind-mounted the data dataset into it from the Proxmox host:

pct set <ctid> -mp0 /tank/data,mp=/mnt/nas

The container can now see everything in tank/data at /mnt/nas, while ZFS stays managed by Proxmox.

Sharing with Samba

Inside the container, I installed Samba and defined a share in /etc/samba/smb.conf pointing at the mounted data:

[nas]
   path = /mnt/nas
   read only = no
   valid users = <username>

Samba keeps its own password database separate from Linux, so I added my user with smbpasswd -a <username> and restarted Samba. After that, the share showed up on every computer on my network.

DNS and VPN

With the NAS running, two problems emerged. Every service was addressed by IP, and nothing was reachable outside my home network. I wanted to solve the second problem without opening ports on my router, which led to Tailscale.

Tailscale creates a private WireGuard network, called a tailnet, between your devices. With Tailscale installed on the Proxmox host and on my phone and laptop, those devices can reach each other from anywhere as if they were on the same LAN. Nothing is exposed to the public internet, and no port forwarding is required.

For hostnames, I deployed Technitium DNS Server in its own container, named dns. Pi-hole and AdGuard Home are the common choices for a homelab, but both are primarily ad-blockers with DNS features. Technitium is a full DNS server with ad-blocking as a feature. Most importantly, it can host a real DNS zone, so I created home.thejordanchoi.com and gave each service a proper hostname, such as nas.home.thejordanchoi.com. Queries for anything outside that zone are forwarded to Cloudflare's 1.1.1.1 and 1.0.0.1.

The final step was configuring Tailscale to use it. In the Tailscale admin console, I set Technitium's Tailscale IP as the primary nameserver, added 1.1.1.1 as a fallback in case the container is unavailable, and enabled Override DNS servers. Every query from a device on the tailnet now follows the same path:

device → Tailscale → Technitium ─┬─ *.home.thejordanchoi.com → answered from my zone
                                 └─ everything else          → forwarded to Cloudflare

Palworld

With networking in place, hosting a game server for friends was a logical next step. Palworld's dedicated server runs on Linux, so it followed the same pattern as the NAS: a dedicated Debian container, named palworld.

The server is distributed through Steam, so the first step was installing SteamCMD, Valve's command-line Steam client, under a dedicated steam user rather than root. A single command then downloads the Palworld dedicated server:

steamcmd +login anonymous +app_update 2394010 validate +quit

After an initial run to generate the configuration files, I set the server name, password, and player limit in PalWorldSettings.ini, then wrapped the server in a systemd service so it starts on boot and restarts automatically after a crash.

Exposing the server publicly

Running the server locally was straightforward. Making it reachable by friends took more work. Requiring them to install a VPN was not an option, so Tailscale could not be used here, and exposing my home IP remained off the table. Cloudflare Tunnel was ruled out because it only carries web traffic, and Tailscale Funnel because it is limited to a few HTTPS ports. The solution was playit.gg. The Palworld container opens an outbound tunnel to playit's infrastructure, and players connect to a playit address, so nothing points to my home network and no ports are open.

For a cleaner address, I added a CNAME record in Cloudflare pointing palworld.thejordanchoi.com to the playit address, with proxying disabled. This worked for everyone outside my network, but not from inside it: connections from home went out to playit and tried to route back in, which my router does not support. The fix was a local record in Technitium that resolves palworld.thejordanchoi.com directly to the container's LAN IP. Devices at home receive the local address, while everyone else receives the public answer from Cloudflare.

Ad-block

Because every DNS query from my devices already passes through Technitium, adding ad-blocking required little additional setup. Blocking is disabled by default, so I enabled it under Settings → Blocking and added the Pro tier of the Hagezi blocklist, which covers roughly 270,000 domains. When a device requests a known ad or tracking domain, Technitium responds that the domain does not exist, and the ad fails to load. This applies to every app on the device, not only the browser. Following setup, I used an ad-block testing site to measure roughly 90-95% of ads being blocked.

DNS-based blocking has limitations. It only applies to devices that use Technitium as their resolver, and it cannot block ads served from the same domain as the content they appear alongside. YouTube ads, for example, are not affected.

Photos UI - Immich

With storage in place, the remaining piece of the original goal was a way to browse photos comparable to Google Photos. Immich is the closest self-hosted equivalent, offering a timeline, albums, face recognition, search, and a mobile app.

The first step was exporting my library from Apple Photos. Using osxphotos, I exported roughly 107 GB, nearly 19,000 photos and videos, to the NAS over the Samba share.

Immich runs in its own container using Docker Compose. I bind-mounted my folder from tank/data into it as read-only:

pct set <ctid> -mp0 /tank/data/<my-folder>,mp=/mnt/photos,ro=1

In Immich, that folder is configured as an external library. Immich indexes the files in place rather than importing copies, so the export remains the source of truth, and the read-only mount ensures Immich cannot modify or delete anything. It also avoids most of the file-permission complications that come with sharing a directory between containers.

The initial scan took considerable time. Immich runs machine learning over every photo to power face recognition and search, and a container with a few gigabytes of RAM processes 19,000 photos slowly. Reducing job concurrency in the admin settings kept the scan stable, and it completed overnight.

Conclusion

What began as an effort to eliminate a $1.99 monthly subscription became a hypervisor, a ZFS mirror, a NAS, a game server, a DNS server, network-wide ad-blocking, a private VPN, and a self-hosted photo library. It was not a cost-effective way to save $1.99, but it was an effective way to learn some systems engineering.

Work remains. The tank/backups dataset is still empty, and a mirror protects against a failed drive, not against theft, disaster, or accidental deletion, so an off-site backup is the next priority. The NAS itself is complete, and the photos that started this project now live on hardware I own.