A personal Raspberry Pi 5 NAS project built with Docker, Nextcloud, MariaDB, Nginx Proxy Manager, and Tailscale.
This project documents my working Raspberry Pi 5 NAS setup, also known as a PiNAS. The goal was to build a private, self-hosted cloud storage system that I can access safely from my own devices without exposing public ports on my home network.
Status: Working personal NAS setup
Main use case: Private cloud storage, file syncing, and remote access
Remote access method: Tailscale private VPN access
Public ports required: None
- Project Overview
- Hardware Used
- Architecture
- Storage Setup
- Docker Setup
- Docker Compose Configuration
- Start the Containers
- Nginx Proxy Manager Setup
- Tailscale Setup
- Nextcloud Trusted Domain Configuration
- Testing the Setup
- Security Notes
- Troubleshooting
- Future Improvements
- Resources
This project turns a Raspberry Pi 5 into a small personal NAS using:
- Raspberry Pi 5
- SATA HAT
- Attached SATA storage
- RAID0 storage mounted at
/mnt/raid - Docker
- Nextcloud
- MariaDB
- Nginx Proxy Manager
- Tailscale
The final result is a self-hosted cloud storage system similar to Google Drive, OneDrive, or Dropbox, except the files are stored on my own hardware.
The setup uses Tailscale so I can access Nextcloud remotely through a private encrypted network instead of opening ports on my router.
- Raspberry Pi 5
- SATA HAT for Raspberry Pi 5
- Mini cooler / cooling solution
- SATA storage drives
- MicroSD card or boot drive
- Power supply for Raspberry Pi 5
- Network connection
- This setup currently uses Wi-Fi 5 GHz
I followed this video for the initial Raspberry Pi 5 SATA HAT hardware setup:
https://www.youtube.com/watch?v=l30sADfDiM8
Make sure to also install any required software or drivers that allow the Raspberry Pi 5 to communicate with the SATA HAT.
Client Device
Browser / Nextcloud Mobile App
|
| HTTPS through Tailscale
v
Tailscale MagicDNS / Tailscale Serve
|
| HTTP internally
v
Nginx Proxy Manager
|
| HTTP reverse proxy
v
Nextcloud Docker Container
|
v
MariaDB Docker Container
|
v
RAID Storage Mounted at /mnt/raid
This setup bypasses CGNAT and avoids public port forwarding by using Tailscale. Tailscale provides private encrypted access to the Raspberry Pi. Nginx Proxy Manager then routes the internal HTTP request to the Nextcloud Docker container.
No public DNS records, Cloudflare tunnels, router port forwarding, or public-facing services are required.
My storage is mounted at:
/mnt/raidThe Nextcloud data directory is stored at:
/mnt/raid/nextcloud-dataInside the Nextcloud container, that folder is mapped to:
/var/www/html/dataFor this project, I used RAID0 to combine storage capacity and use all available drive space.
Important: RAID0 is not a backup. If one drive fails, the entire array can be lost. RAID0 is useful for capacity and performance, but not for redundancy.
If you want redundancy, consider:
- RAID1
- RAID5 / RAIDZ
- External USB backup drive
- Another NAS backup
- Cloud backup for critical files
Create the Nextcloud data directory if it does not already exist:
sudo mkdir -p /mnt/raid/nextcloud-dataSet ownership so the Nextcloud Apache container can write to it:
sudo chown -R www-data:www-data /mnt/raid/nextcloud-dataIf needed, set permissions:
sudo chmod -R 750 /mnt/raid/nextcloud-dataVerify the mount:
df -hYou should see /mnt/raid listed.
First, check whether Docker and Docker Compose are already installed:
docker --versiondocker compose versionUpdate the system:
sudo apt update && sudo apt upgrade -yInstall required packages:
sudo apt-get install ca-certificates curl gnupg lsb-release -yCreate the Docker keyrings folder:
sudo mkdir -p /etc/apt/keyringsAdd Docker's official GPG key:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpgSet permissions:
sudo chmod a+r /etc/apt/keyrings/docker.gpgAdd the Docker repository:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/nullUpdate package lists:
sudo apt-get updateInstall Docker:
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -yVerify installation:
docker --versiondocker compose versionOptional: allow your user to run Docker commands without typing sudo:
sudo usermod -aG docker $USERThen log out and log back in.
Create a project folder:
mkdir -p ~/nextcloudMove into it:
cd ~/nextcloudCreate the Docker Compose file:
nano docker-compose.ymlPaste the following:
version: "3.8"
services:
db:
image: mariadb:10.11
container_name: nextcloud_db
restart: always
environment:
MYSQL_ROOT_PASSWORD: "CHANGE_THIS_ROOT_PASSWORD"
MYSQL_DATABASE: "nextcloud"
MYSQL_USER: "nextcloud"
MYSQL_PASSWORD: "CHANGE_THIS_DATABASE_PASSWORD"
volumes:
- db_data:/var/lib/mysql
networks:
- web
nextcloud:
image: nextcloud:32-apache
container_name: nextcloud
restart: always
depends_on:
- db
environment:
MYSQL_HOST: db
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
MYSQL_PASSWORD: "CHANGE_THIS_DATABASE_PASSWORD"
volumes:
- nextcloud_html:/var/www/html
- /mnt/raid/nextcloud-data:/var/www/html/data
networks:
- web
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: always
dns:
- 1.1.1.1
- 9.9.9.9
ports:
- "80:80"
- "81:81"
# Port 443 is not needed here because Tailscale provides HTTPS access.
volumes:
- npm_data:/data
networks:
- web
volumes:
db_data:
nextcloud_html:
npm_data:
networks:
web:
driver: bridgeImportant: Do not commit real passwords to GitHub. Replace the placeholder passwords before running the containers.
Start everything:
docker compose up -dCheck that the containers are running:
docker psYou should see containers similar to:
nextcloud_db
nextcloud
nginx-proxy-manager
View logs if something does not start correctly:
docker compose logsOr view logs for a specific container:
docker logs nextclouddocker logs nextcloud_dbdocker logs nginx-proxy-managerOpen Nginx Proxy Manager from another machine on your LAN:
http://<PI_LAN_IP>:81
Find the Raspberry Pi's LAN IP with:
hostname -IOr:
ip addrThe default Nginx Proxy Manager login is:
Email: admin@example.com
Password: changeme
After logging in, immediately change the default email and password.
In Nginx Proxy Manager, go to:
Hosts -> Proxy Hosts -> Add Proxy Host
Use the following settings:
Domain Names: nextcloud.local
Scheme: http
Forward Hostname / IP: nextcloud
Forward Port: 80
Enable:
Websockets Support
Block Common Exploits
Leave SSL off in Nginx Proxy Manager because Tailscale will handle HTTPS externally.
Save the proxy host and wait for it to show as online.
Later, after Tailscale is fully configured, replace nextcloud.local with your Tailscale HTTPS hostname, which will look similar to:
your-pi-name.your-tailnet-name.ts.net
Install Tailscale:
curl -fsSL https://tailscale.com/install.sh | shStart and authenticate Tailscale:
sudo tailscale upTailscale should print a login link. Open the link in your browser and sign in.
Check the Tailscale connection:
tailscale statusYour Raspberry Pi should show as connected.
In the Tailscale admin console:
DNS -> MagicDNS -> Enable
In the Tailscale admin console, enable HTTPS certificates for your tailnet.
Tailscale Serve requires HTTPS certificates before it can provide HTTPS access to a local service.
Once Nginx Proxy Manager is listening on local port 80, use Tailscale Serve to expose that internal service privately to devices in your tailnet:
sudo tailscale serve --bg http://127.0.0.1:80Check the Serve status:
tailscale serve statusYou should see a Tailscale HTTPS URL similar to:
https://your-pi-name.your-tailnet-name.ts.net
Use that full hostname in Nginx Proxy Manager as the proxy host domain name.
This is private to your Tailscale network. It is not the same as Tailscale Funnel, which would make a service public.
Nextcloud may block access until the Tailscale hostname is added as a trusted domain.
Enter the Nextcloud container:
docker exec -it nextcloud bashRun the following commands inside the container.
Add the Tailscale hostname as a trusted domain:
php occ config:system:set trusted_domains 1 --value="your-pi-name.your-tailnet-name.ts.net"Set the overwrite protocol to HTTPS:
php occ config:system:set overwriteprotocol --value="https"Set the overwrite host:
php occ config:system:set overwritehost --value="your-pi-name.your-tailnet-name.ts.net"Exit the container:
exitRestart Nextcloud:
docker restart nextcloudNow open:
https://your-pi-name.your-tailnet-name.ts.net
docker psExpected result:
nextcloud_db
nextcloud
nginx-proxy-manager
http://<PI_LAN_IP>:81
tailscale statustailscale serve statusFrom a device signed into the same Tailscale network, open:
https://your-pi-name.your-tailnet-name.ts.net
You should see the Nextcloud setup page or login page.
This setup was designed to avoid exposing the NAS directly to the public internet.
Security choices used in this project:
- No router port forwarding
- No public DNS required
- Tailscale private network access
- HTTPS provided through Tailscale
- Nginx Proxy Manager used internally
- MariaDB separated into its own Docker container
- Nextcloud data stored outside the container on mounted RAID storage
- Default Nginx Proxy Manager credentials changed after first login
- Placeholder passwords used in the public README
Recommended additional security improvements:
- Enable 2FA on the Tailscale account
- Enable 2FA on the Nextcloud admin account
- Use strong unique database passwords
- Do not commit secrets to GitHub
- Keep Docker images updated
- Keep Raspberry Pi OS updated
- Use external backups for important files
- Consider disabling or firewalling Nginx Proxy Manager admin port
81after setup - Use a UPS if the NAS stores important data
Check container status:
docker ps -aView logs:
docker compose logsRestart:
docker compose restartCheck that port 81 is mapped:
docker psTry opening:
http://<PI_LAN_IP>:81
Make sure the Raspberry Pi firewall is not blocking port 81 on the LAN.
Add your Tailscale hostname to Nextcloud trusted domains:
docker exec -it nextcloud bash
php occ config:system:set trusted_domains 1 --value="your-pi-name.your-tailnet-name.ts.net"
exit
docker restart nextcloudSet overwrite protocol:
docker exec -it nextcloud bash
php occ config:system:set overwriteprotocol --value="https"
php occ config:system:set overwritehost --value="your-pi-name.your-tailnet-name.ts.net"
exit
docker restart nextcloudCheck Tailscale status:
tailscale statusCheck Serve status:
tailscale serve statusMake sure MagicDNS and HTTPS certificates are enabled in the Tailscale admin console.
Check mounts:
df -hCheck block devices:
lsblkIf /mnt/raid is not mounted, verify the /etc/fstab entry for the RAID array.
Reset ownership:
sudo chown -R www-data:www-data /mnt/raid/nextcloud-dataRestart Nextcloud:
docker restart nextcloudPossible upgrades for this project:
- Move from Wi-Fi to Ethernet for better NAS performance
- Add automated external backups
- Switch from RAID0 to RAID1 or another redundant storage setup
- Add monitoring with Uptime Kuma, Grafana, or Netdata
- Add a UPS for safer shutdowns during power loss
- Use Docker
.envfiles or Docker secrets instead of hardcoded passwords - Add firewall rules to restrict access to Nginx Proxy Manager admin port
- Raspberry Pi 5 SATA HAT setup video: https://www.youtube.com/watch?v=l30sADfDiM8
- Docker documentation: https://docs.docker.com/
- Nextcloud documentation: https://docs.nextcloud.com/
- MariaDB Docker image: https://hub.docker.com/_/mariadb
- Nextcloud Docker image: https://hub.docker.com/_/nextcloud
- Nginx Proxy Manager: https://nginxproxymanager.com/
- Tailscale documentation: https://tailscale.com/docs/
- Tailscale Serve documentation: https://tailscale.com/docs/features/tailscale-serve
This project is for personal homelab and learning purposes. Anyone following this setup should use strong passwords, keep backups, and understand that RAID0 does not protect against drive failure.