Your next NAS is not a NAS

📢 This article was translated by Gemini-2.5-pro

Introduction

Back in 2022, when I was first messing with Linux(Simplify Chinese) , I had this idea that you don’t necessarily have to pay for a special OS just to build a NAS.

Fast forward to today. While tinkering with my server, the mount speeds on my cloud drives have been just… disappointing. On impulse, I ordered an N5095 motherboard. I realized the 4GB RAM stick I have lying around is a bit low, but I really didn’t want to spend any more money.

And so, my tinkering journey began again.

Tech Choices

First up, the OS. For this low-spec setup, the lightest choice is obvious: Alpine Linux.

Next, how to install it. To stay compatible with WEPE , I used Ventoy as the main bootloader to install the system.

Services are all deployed using Docker to keep the host system clean and make everything reproducible.

Services I’m using:

Services I considered but skipped:

File Structure

My usual style for service locations: each service gets its own folder under /home.

All HDDs are mounted under /mnt, which is pretty standard.

I use mount –bind to “bind” the actual files on the HDDs to the locations where the services expect them.

pic

System Installation

Download Alpine Linux from the official site , put it on a Ventoy USB drive. Set the motherboard BIOS to boot from USB, select Alpine Linux from the Ventoy menu, and boot.

After it finishes loading, log in as root (no password needed). Once logged in, run the install command:

1
setup-alpine

Just follow the prompts to complete the installation.

Official docs: setup-alpine - Alpine Linux

Public Key Login

Edit the SSH config. First, create the .ssh folder.

1
mkdir .ssh

Go into the folder:

1
cd .ssh

Add your public key to authorized_keys.

1
vi authorized_keys

(Your public key is in %USERPROFILE%\.ssh on Windows)

Enable public key authentication in the SSH daemon config:

1
vi /etc/ssh/sshd_config

Around line 41, uncomment the line so it says:

1
PubkeyAuthentication yes

Enable Community Repository

Open the repositories file:

1
vi /etc/apk/repositories

Remove the comment (#) from the community line:

1
2
3
#/media/dm-0/apks
http://dl-cdn.alpinelinux.org/alpine/v3.22/main
http://dl-cdn.alpinelinux.org/alpine/v3.22/community

Install Docker

Update packages:

1
apk update

Install Docker:

1
apk add docker docker-cli-compose

Set it to start on boot:

1
rc-update add docker default

Start Docker:

1
service docker start

Check if it’s running:

1
docker ps

Add Swap

The server image didn’t have swap by default, so I added it. (Though I later realized it did add some, so this is optional depending on your needs).

Create a swap file:

1
fallocate -l 8G /swapfile

Set permissions to root-only:

1
chmod 600 /swapfile

Format as swap:

1
mkswap /swapfile

Enable the swap:

1
swapon /swapfile

Check if it’s active:

1
free -h

Mount Hard Drives

Check all block devices:

1
fdisk -l

Install NTFS support:

1
apk add ntfs-3g

Create a mount point folder:

1
mkdir /mnt/hc550

Mount the drive:

1
mount -t ntfs-3g /dev/sdc1 /mnt/hc550

To unmount, you can use the path (recommended):

1
umount /mnt/hc550

Or the device name:

1
umount /dev/sdc1

Auto-mount on Boot

Get the partition’s UUID. This prevents issues if the device name (/dev/sdc1) changes on reboot.

1
blkid

Edit /etc/fstab:

1
vi /etc/fstab

Add this line (using your UUID):

1
2
# <Device-UUID> <Mount-Point> <File-System> <Options>                    <dump> <pass>
UUID=xxxxxxxx   /mnt/hc550    ntfs-3g     defaults,uid=1000,gid=1000 0 0

Test if it worked. First, unmount:

1
umount /dev/sdc1

Now, mount all entries in fstab:

1
mount -a

Check if you can see your files:

1
ls /mnt/hc550

SMB Share

Using Samba, you can mount the drive in Windows and use it just like a local drive.

In /home/samba, create a docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
version: '3.1'
services:
  samba:
    image: 'ghcr.io/crazy-max/samba:latest'
    container_name: samba
    network_mode: host
    volumes:
      - '/home/samba/data:/data'
      - '/mnt/hdd4t:/mount/hdd4t'
      - '/mnt/hc550:/mount/hc550'
    environment:
      - 'TZ=Japan/Tokyo'
      - 'SAMBA_LOG_LEVEL=0'
    restart: always

Edit the Samba config file config.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
auth:
  - user: yexca
    group: yexca
    uid: 1000
    gid: 1000
    password: passwd

global:
  - "force user = yexca"
  - "force group = yexca"

share:
  - name: HDD4TB
    comment: hdd4t
    path: /mount/hdd4t
    browsable: yes
    readonly: no
    guestok: no
    veto: no
    validusers: yexca
    writelist: yexca
    recycle: yes
  - name: HC550
    comment: hc550
    path: /mount/hc550
    browsable: yes
    readonly: no
    guestok: no
    veto: no
    validusers: yexca
    writelist: yexca
    recycle: yes

Mounting as a Drive in Windows

Press Win + R, type cmd, and enter the command:

1
net use Z: \\alpine-nas\HDD4TB /user:yexca passwd /persistent:yes

Refresh Explorer:

1
explorer.exe Z:

Or you can just add it through the File Explorer GUI.

Bind Mount

Compared to a symbolic link, a bind mount is compatible with Docker.

1
mount --bind /mnt/hc550/anime /anime

To make this automatic on boot, add it to /etc/fstab. It must be after the main drive mount line (because the file is processed in order).

1
2
/mnt/hc550/anime   /tmp/anime   none   bind   0 0
/mnt/hc550/comic   /tmp/comic   none   bind   0 0

Verify:

1
df -h

Other Services

As for the other services, I feel like I’ve written about them plenty in my previous posts, so I won’t repeat myself here.

Related articles(Simplify Chinese):

Custom Domain

I use my OpenWRT router’s DNS Hijacking feature for this. It lets me visit a specific domain (like emby.mynas) and have it point to my NAS.

The requirement is that your devices use the router as their DNS server. Then, in the router’s DHCP/DNS settings, give the NAS a static IP and add the custom domains, pointing them all to the NAS’s IP.

Nginx-UI

To make the custom domains work, you need a reverse proxy. I use Nginx.

Docker Compose file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
version: '3.1' 
services: 
  nginx-ui:
    image: uozi/nginx-ui:v2.2.0-patch.1
    container_name: nginx_UI
    volumes:
      - /home/nginxUI/nginx:/etc/nginx
      - /home/nginxUI/nginx-ui:/etc/nginx-ui
      - /home/nginxUI/www:/www
    environment:
      - NGINX_UI_IGNORE_DOCKER_SOCKET=true
    ports:
      - 80:80
      - 443:443
    restart: always

When a container needs to access another container, its bridge IP is 172.17.0.1.

So, if you have a service mapped to port 8888 on the host, the reverse proxy address isn’t 127.0.0.1:8888, it’s 172.17.0.1:8888.

(See: 2025 New Server Setup Log#Installing-nginx-ui(Simplify Chinese) for why I set that environment variable).

Conclusion

I feel like I’ve finally achieved what I was talking about back in 2022.

It’s also weird… in just three years, even though I subjectively feel like I haven’t changed at all, when I read my old articles and remember my situation when I wrote them, I can truly feel that my way of thinking has changed, top to bottom.

To be honest, back then I was probably thinking of using a specialized system like True NAS. But I never would have imagined that when I finally did it, I would use a “from scratch” method that my past self wouldn’t have even dared to try. Looking back at my old posts, piecing this all together… I can genuinely feel the “value” in it.

The tinkering never stops, and neither does the passion.

yexca-261

This post is licensed under CC BY-NC-SA 4.0 by the author.