Skip to content

Instantly share code, notes, and snippets.

@Josh5
Last active July 31, 2021 21:23
Show Gist options
  • Save Josh5/7f05e8383d7c8a76e511fd1764b0de2c to your computer and use it in GitHub Desktop.
Save Josh5/7f05e8383d7c8a76e511fd1764b0de2c to your computer and use it in GitHub Desktop.
NoFrillsNAS

The No-Frills NAS

This is the setup process for the no-frills NAS.

Before running this, you need to have Ubuntu 20.04 installed on a USB stick.

After booting Ubuntu, before doing anything else, follow these steps:

Manditory initial setup

Update all packages

apt update && apt install upgrade

Disable automounting as your primary user

Login to your primary user and run this:

gsettings set org.gnome.desktop.media-handling automount false

This disables auto-mounting devices that are plugged in. This setup will handle that as a root user.

Config file

This system has some configureation options that is read during the install/update processes. Be sure to create this file with any modifications that you may need...

Create a file called /opt/NoFrillsNAS/config.env. Populate it with the following options as required...

Blacklist partitions by UUID

Generate configuration for any cache disks (WIP)

You can have a cache disk present. This needs to be formatted into 2 partitions. One partition (part 1) will be mounted as a directly available mount in /storage/cache The other partition (part 2) will be mounted within the MergerFS pool.

Run the command

blkid

Fetch the UUID of the partitions of the cache disk

Add the variables in the config.env file as shown below

CACHE_DISK_PART1_UUID=90c53f7c-8834-db9a-adb7-f7676bad4e4c
CACHE_DISK_PART2_UUID=837ab7b3-9222-48fd-8508-00b679517cab

Optional initial setup

Some of the parts below are only needed depending on your hardware...

NVIDIA Drivers

# First install deps
apt install dkms pkg-config libglvnd-dev

# Download and install drivers
mkdir -p /opt/nvidia
cd /opt/nvidia
wget https://international.download.nvidia.com/XFree86/Linux-x86_64/455.45.01/NVIDIA-Linux-x86_64-455.45.01.run
chmod +x ./NVIDIA-Linux-x86_64-455.45.01.run
./NVIDIA-Linux-x86_64-455.45.01.run

# Patch the installed driver
wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh
bash ./patch.sh
---
version: '2'
services:
portainer:
image: portainer/portainer-ce
container_name: portainer
command: -l service=system -H unix:///var/run/docker.sock
labels:
- "service=system"
ports:
- 9000:9000
volumes:
- /etc/localtime:/etc/localtime:ro
- /opt/NoFrillsNAS/appdata/portainer:/data
- /var/run/docker.sock:/var/run/docker.sock
restart:
always
yacht:
image: selfhostedpro/yacht
container_name: yacht
labels:
- "service=system"
ports:
- 8000:8000
volumes:
- /opt/NoFrillsNAS/appdata/yacht:/config
- /var/run/docker.sock:/var/run/docker.sock
- /opt/NoFrillsNAS/docker-compose-files:/docker-compose-files
environment:
- COMPOSE_DIR=/docker-compose-files/
restart: always
docker-compose-ui:
image: rururukenken/docker-compose-ui:latest
container_name: docker-compose-ui
labels:
- "service=system"
working_dir: /opt/NoFrillsNAS/docker-compose-files/
volumes:
- /opt/NoFrillsNAS/appdata/docker-compose-ui:/config
- /var/run/docker.sock:/var/run/docker.sock
- /opt/NoFrillsNAS/docker-compose-files:/opt/NoFrillsNAS/docker-compose-files
ports:
- 5000:5000
restart: always
heimdall:
image: ghcr.io/linuxserver/heimdall
container_name: heimdall
labels:
- "service=system"
environment:
- PUID=1000
- PGID=1000
- TZ=HOST_TIMEZONE
volumes:
- /opt/NoFrillsNAS/appdata/heimdall:/config
ports:
- 8080:80
- 443:443
restart: always
organizr:
image: linuxserver/organizr
container_name: organizr
labels:
- "service=system"
environment:
- PUID=1000
- PGID=1000
- TZ=HOST_TIMEZONE
volumes:
- /opt/NoFrillsNAS/appdata/organizr:/config
ports:
- 80:80
restart: always
#!/bin/bash
#
# usage:
# wget -O - https://gist.githubusercontent.com/Josh5/7f05e8383d7c8a76e511fd1764b0de2c/raw/install.sh | bash
#
# Ensure we are run as root...
if [[ $(id -u) -gt 0 ]]; then
echo "ERROR! This needs to be run as root... Exiting..."
exit 1
fi
echo
echo "Installing/Upgrading No Frills NAS Config"
echo
echo
_INSTALL_PATH="/opt/NoFrillsNAS"
mkdir -p ${_INSTALL_PATH}
chmod a+rw ${_INSTALL_PATH}
_MOUNT_SCRIPT=$(cat << 'EOF'
#!/bin/bash
ACTION=$1
DEVBASE=$2
DEVICE="/dev/${DEVBASE}"
STORAGE_POOL_PATH=/storage/pool
# See if this drive is already mounted
MOUNT_POINT=$(/bin/mount | /bin/grep ${DEVICE} | /usr/bin/awk '{ print $3 }')
do_mount()
{
if [[ -n ${MOUNT_POINT} ]]; then
# Already mounted, exit
echo "Ignoring due to device already mounted"
exit 1
fi
if [[ -e /opt/NoFrillsNAS/config.env ]]; then
source /opt/NoFrillsNAS/config.env
fi
# Get info for this drive: $ID_FS_LABEL, $ID_FS_UUID, and $ID_FS_TYPE
# This replaces whitespace with underscore
BLKID_DATA="$(/sbin/blkid -o udev ${DEVICE})"
eval $(echo "${BLKID_DATA// /_}")
# Blacklist of lables...
if [[ "$ID_FS_LABEL_FATBOOT" =~ ^(EFI|BOOT)$ ]]; then
# Dont mount, exit
echo "Ignoring due to ID_FS_LABEL_FATBOOT"
exit 1
fi
if [[ "$ID_FS_LABEL" =~ ^(EFI|BOOT|SYSTEM|Recovery|RECOVERY|SETTINGS|boot|root0|share0)$ ]]; then
# Dont mount, exit
echo "Ignoring due to ID_FS_LABEL"
exit 1
fi
if [[ " ${DISK_UUID_BLACKLIST[@]} " =~ " ${ID_FS_UUID} " ]]; then
# Dont mount, exit
echo "Ignoring due to ID_FS_UUID in config.env file DISK_UUID_BLACKLIST list"
exit 1
fi
# Figure out a mount point to use
LABEL=${ID_FS_LABEL}
if [[ -z "${LABEL}" ]]; then
LABEL=${DEVBASE}
elif /bin/grep -q " /media/${LABEL} " /etc/mtab; then
# Already in use, make a unique one
LABEL+="-${DEVBASE}"
fi
LABEL=$(echo "${LABEL// /_}")
MOUNT_POINT="/mnt/disks/${LABEL}"
/bin/mkdir -p ${MOUNT_POINT}
# Global mount options
OPTS="rw,relatime"
# File system type specific mount options
if [[ ${ID_FS_TYPE} == "vfat" || ${ID_FS_TYPE} == "ntfs" ]]; then
OPTS+=",users,gid=100,umask=000,shortname=mixed,utf8=1,flush"
fi
if ! /bin/mount -o ${OPTS} ${DEVICE} ${MOUNT_POINT}; then
# Error during mount process: cleanup mountpoint
/bin/rmdir ${MOUNT_POINT}
exit 1
fi
# Add it to the storage pool
if ! `/usr/bin/grep "${STORAGE_POOL_PATH}" /proc/mounts &> /dev/null` ; then
echo "Could not find mergerfs mount.. Lets create it!"
mkdir -p /storage/pool
mergerfs -o defaults,allow_other,direct_io,use_ino,minfreespace=1G,fsname=mergerfs '/mnt/disks/*' /storage/pool
fi
xattr -w user.mergerfs.srcmounts "+>${MOUNT_POINT}" ${STORAGE_POOL_PATH}/.mergerfs
}
do_checkmounts()
{
# Delete all empty dirs in /mnt/disks that aren't being used as mount points.
for f in /mnt/disks/* ; do
if ! /bin/grep -q " $f " /etc/mtab; then
# This directory is not a mount...
# Remove all instances of it from the storage pool
if `/usr/bin/grep "mergerfs ${STORAGE_POOL_PATH}" /proc/mounts &> /dev/null` ; then
xattr -w user.mergerfs.srcmounts "-${f}" ${STORAGE_POOL_PATH}/.mergerfs
fi
# Remove the directory if it is empty
if [[ -n $(/usr/bin/find "$f" -maxdepth 0 -type d -empty) ]]; then
/bin/rmdir "$f"
fi
else
# This directory is a mount...
# Check that it is already in the pool
if ! `xattr -p user.mergerfs.srcmounts /storage/pool/.mergerfs | grep ${f} &> /dev/null` ; then
# Not already in the pool, time to add it I suppose...
xattr -w user.mergerfs.srcmounts "+>${f}" ${STORAGE_POOL_PATH}/.mergerfs
fi
fi
done
}
do_unmount()
{
if [[ -n ${MOUNT_POINT} ]]; then
/bin/umount -l ${DEVICE}
fi
do_checkmounts
}
case "${ACTION}" in
add)
do_mount
;;
remove)
do_unmount
;;
check)
do_checkmounts
;;
esac
EOF
)
_UDEV_RULES=$(cat << 'EOF'
# check for blockdevices, /dev/sd*, /dev/sr*, /dev/mmc*, and /dev/nvme*
SUBSYSTEM!="block", KERNEL!="sd*|sr*|mmc*|nvme*", GOTO="modifications"
# check for special partitions we dont want mount
IMPORT{builtin}="blkid"
ENV{ID_FS_LABEL}=="EFI|BOOT|SYSTEM|Recovery|RECOVERY|SETTINGS|boot|root0|share0", GOTO="exit"
ENV{ID_FS_LABEL_FATBOOT}=="EFI|BOOT", GOTO="exit"
ENV{ID_FS_TYPE}=="vfat", GOTO="exit"
# /dev/sd*, /dev/mmc*, and /dev/nvme* with partitions/disk and filesystems only, and /dev/sr* disks only
KERNEL=="sd*|mmc*|nvme*", ENV{DEVTYPE}=="partition|disk", ENV{ID_FS_USAGE}=="filesystem", GOTO="harddisk"
GOTO="exit"
# mount or umount for hdds
LABEL="harddisk"
ACTION=="add", PROGRAM="/usr/bin/sh -c '/usr/bin/grep -E ^/dev/%k\ /proc/mounts || true'", RESULT=="", RUN+="/bin/systemctl start disk-mount@%k.service"
ACTION=="remove", RUN+="/bin/systemctl stop disk-mount@%k.service"
GOTO="exit"
# Modifications to mounts
LABEL="modifications"
SUBSYSTEM!="bdi", GOTO="exit"
ACTION=="add", RUN+="/usr/local/bin/disk_mount check"
ACTION=="remove", RUN+="/usr/local/bin/disk_mount check"
GOTO="exit"
# Exit
LABEL="exit"
EOF
)
_SYSTEMD_DISK_MOUNT_UNIT=$(cat << 'EOF'
[Unit]
Description=Mount Drive on %i
[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/usr/local/bin/disk_mount add %i
ExecStop=/usr/local/bin/disk_mount remove %i
EOF
)
_SAMBA_SHARE_CONFIG=$(cat << 'EOF'
## NoFrillsNAS Samba Configuration
[global]
server string = NoFrillsNAS
browseable = yes
writeable = yes
printable = no
deadtime = 30
mangled names = no
name resolve order = host bcast
printcap name = /dev/null
load printers = no
enable core files = no
passdb backend = smbpasswd
smb encrypt = disabled
fruit:model = Xserve
# samba share options
map to guest = Bad User
guest account = root
security = user
# samba tuning options
socket options = TCP_NODELAY IPTOS_LOWDELAY
min receivefile size = 16384
aio read size = 16384
aio write size = 16384
use sendfile = yes
# Log config
log file = /var/log/samba/%m.log
max log size = 50
# Samba Shares
[config]
comment = opt directory
path = /opt/NoFrillsNAS
browseable = yes
read only = no
guest ok = yes
[storage]
comment = Storage on NoFrillsNAS
path = /storage
browseable = yes
read only = no
guest ok = yes
EOF
)
_NFS_SHARE_CONFIG=$(cat << 'EOF'
# /etc/exports
# Usually I only use NFS in a read-only situation and this file is configured
# based on that assumption (primarily KODI).
# http://kodi.wiki/view/NFS
/storage/pool *(fsid=0,ro,sync,no_root_squash,no_subtree_check)
EOF
)
function build_fstab {
if [[ ! -e /etc/fstab.original ]]; then
cp -v /etc/fstab /etc/fstab.original
fi
original_fstab=$(cat /etc/fstab.original)
echo "${original_fstab}" > /etc/fstab
echo "" >> /etc/fstab
echo "# TMPFS for mount points:" >> /etc/fstab
# Create a mount for our merged storage
echo "tmpfs /storage tmpfs nosuid,nodev,noatime 0 0" >> /etc/fstab
# Create a mount point for our disks
echo "tmpfs /mnt/disks tmpfs nosuid,nodev,noatime 0 0" >> /etc/fstab
# Create an initial ramdisk mount of really low limited size for the initial mergerfs pool
echo "tmpfs /mnt/disks/ramdisk tmpfs nosuid,nodev,noatime,size=12k 0 0" >> /etc/fstab
echo "" >> /etc/fstab
if [[ -e ${_INSTALL_PATH}/config.env ]]; then
source ${_INSTALL_PATH}/config.env
if [[ ! -z ${CACHE_DISK_PART1_UUID} ]]; then
echo "# MergerFS Cache:" >> /etc/fstab
cache_disk_part1_fs_type=$(blkid -o value -s TYPE /dev/disk/by-uuid/${CACHE_DISK_PART1_UUID})
echo "/dev/disk/by-uuid/${CACHE_DISK_PART1_UUID} /storage/cache ${cache_disk_part1_fs_type} defaults 0 0" >> /etc/fstab
echo "" >> /etc/fstab
else
echo "# (No cache disk part1 configured)" >> /etc/fstab
echo "" >> /etc/fstab
fi
if [[ ! -z ${CACHE_DISK_PART2_UUID} ]]; then
echo "# MergerFS Cache:" >> /etc/fstab
cache_disk_part2_fs_type=$(blkid -o value -s TYPE /dev/disk/by-uuid/${CACHE_DISK_PART2_UUID})
echo "/dev/disk/by-uuid/${CACHE_DISK_PART2_UUID} /mnt/disks/cache ${cache_disk_part2_fs_type} defaults 0 0" >> /etc/fstab
echo "" >> /etc/fstab
else
echo "# (No cache disk part2 configured)" >> /etc/fstab
echo "" >> /etc/fstab
fi
else
echo "# (No config file found)" >> /etc/fstab
echo "" >> /etc/fstab
fi
# # [WIP] Detect all current disks - If they are internal, then add them to fstab
# echo "# Data disks:" >> /etc/fstab
# for disk_uuid in `ls /dev/disk/by-uuid/ 2> /dev/null`; do
# if [[ " ${DISK_UUID_BLACKLIST[@]} " =~ " ${disk_uuid} " ]]; then
# # Dont add to fstab
# echo "# Ignoring disk '${disk_uuid}' due to config.env file DISK_UUID_BLACKLIST list"
# continue
# fi
# # Ensure that they are not already in the fstab file
# if `grep "${disk_uuid}" /etc/fstab &> /dev/null` ; then
# # Dont add duplicate to fstab
# echo "# Ignoring disk '${disk_uuid}' due to UUID existing in fstab"
# continue
# fi
# # Get the disk FS type
# disk_fs_type=$(blkid -o value -s TYPE /dev/disk/by-uuid/${disk_uuid})
# # If that failed, dont bother adding it to fstab
# [[ -z ${disk_fs_type} ]] && continue
# # Don't add vfat FS partitions to the fstab file
# if [[ "${disk_fs_type,,}" =~ ^(vfat|swap|crypto_luks)$ ]]; then
# # Dont add duplicate to fstab
# echo "# Ignoring disk '${disk_uuid}' due to fs type being '${disk_fs_type}'"
# continue
# fi
# # Get the disk FS type
# disk_part_label=$(blkid -o value -s LABEL /dev/disk/by-uuid/${disk_uuid})
# if [[ "${disk_part_label,,}" =~ ^(efi|boot|system|recovery|settings|boot|root0|share0)$ ]]; then
# # Dont add duplicate to fstab
# echo "# Ignoring disk '${disk_uuid}' due to part label being '${disk_part_label}'"
# continue
# fi
# # Figure out a mount point to use
# ## Find out devbase
# devbase=$( basename $( readlink -e /dev/disk/by-uuid/${disk_uuid} ) )
# mount_label=${disk_part_label}
# if [[ -z "${mount_label}" ]]; then
# LABEL=${devbase}
# elif /bin/grep -q " /media/${LABEL} " /etc/mtab; then
# # Already in use, make a unique one
# mount_label+="-${devbase}"
# fi
# mount_label=$(echo "${mount_label// /_}")
# mount_point="/mnt/disks/${mount_label}"
# # Global mount options
# OPTS="rw,relatime"
# # File system type specific mount options
# if [[ "${disk_fs_type,,}" =~ ^(vfat|ntfs)$ ]]; then
# OPTS+=",users,gid=100,umask=000,shortname=mixed,utf8=1,flush"
# fi
# # Create fstab entry
# echo "/dev/disk/by-uuid/${disk_uuid} ${mount_point} ${disk_fs_type} defaults,nofail,${OPTS} 0 0" >> /etc/fstab
# done
# disk_fs_type=""
# echo "" >> /etc/fstab
# Build MergerFS /storage/pool mount args
storage_pool_mount_options="defaults,allow_other,direct_io,use_ino,minfreespace=1G,fsname=mergerfs"
# Specify the create policy as mspmfs - most free space
storage_pool_mount_options="${storage_pool_mount_options},category.create=mfs"
# # [TODO] Specify the policy for ENOSPC (no space left on device) or EDQUOT (disk quota exceeded) as mfs - most free space
# storage_pool_mount_options="${storage_pool_mount_options},moveonenospc=true"
# Specify the policy for file caching as partial - Enables page caching. Underlying files cached, mergerfs files cached while open.
storage_pool_mount_options="${storage_pool_mount_options},cache.files=partial,dropcacheonclose=true"
echo "# MergerFS Mounts:" >> /etc/fstab
echo "/mnt/disks/* /storage/pool fuse.mergerfs ${storage_pool_mount_options} 0 0" >> /etc/fstab
echo "" >> /etc/fstab
}
# Install the mount script
echo " ... Installing mount scripts ..."
echo "${_MOUNT_SCRIPT}" > /usr/local/bin/disk_mount
chmod 777 /usr/local/bin/disk_mount
chown root:root /usr/local/bin/disk_mount
echo
# Install systemd unit
echo " ... Installing systemd unit ..."
echo "${_SYSTEMD_DISK_MOUNT_UNIT}" > /etc/systemd/system/[email protected]
chmod 644 /etc/systemd/system/[email protected]
chown root:root /etc/systemd/system/[email protected]
systemctl daemon-reload
echo
# Install the UDEV rules
echo " ... Installing UDEV rules ..."
echo "${_UDEV_RULES}" > /etc/udev/rules.d/99-media-automount.rules
chmod 664 /etc/udev/rules.d/99-media-automount.rules
chown root:root /etc/udev/rules.d/99-media-automount.rules
udevadm control --reload-rules
echo
# Ensure MergerFS is installed
`command -v mergerfs &> /dev/null` || apt_deps_list="${apt_deps_list} mergerfs fuse xattr"
`command -v fusermount &> /dev/null` || apt_deps_list="${apt_deps_list} fuse"
`command -v xattr &> /dev/null` || apt_deps_list="${apt_deps_list} xattr"
# Build the initial mounts in fstab
echo " ... Installing fstab mount config ..."
build_fstab
echo
# Ensure Docker is installed
if ! `command -v docker &> /dev/null`; then
echo " ... Found missing dep: 'docker' ... "
echo
echo " ... Installing docker-ce ..."
curl https://get.docker.com | sh
systemctl restart docker
echo
fi
id -a 1000 | grep '(docker)' &> /dev/null || usermod -aG docker shuttle
# Ensure Docker Compose is installed
`command -v pip3 &> /dev/null` || apt_deps_list="${apt_deps_list} python3-pip"
if ! `command -v docker-compose &> /dev/null`; then
echo " ... Found missing dep: 'docker-compose' ..."
`command -v docker-compose &> /dev/null` || pip3_deps_list="${pip3_deps_list} docker-compose"
/etc/environment
echo
fi
grep -q "PUID=1000" /etc/environment 2> /dev/null || echo "PUID=1000" >> /etc/environment
grep -q "PGID=1000" /etc/environment 2> /dev/null || echo "PGID=1000" >> /etc/environment
# Ensure network share servers are installed
`dpkg -la | grep samba &> /dev/null` || apt_deps_list="${apt_deps_list} samba"
`dpkg -la | grep nfs-kernel-server &> /dev/null` || apt_deps_list="${apt_deps_list} nfs-kernel-server"
# Ensure cockpit is installed
`dpkg -la | grep cockpit &> /dev/null` || apt_deps_list="${apt_deps_list} cockpit"
`dpkg -la | grep cockpit-docker &> /dev/null` || dpkg_deps_list="${dpkg_deps_list} /opt/cockpit/cockpit-docker_215-1~ubuntu19.10.1_all.deb"
mkdir -p /opt/cockpit
if [[ ! -e /opt/cockpit/cockpit-docker_215-1~ubuntu19.10.1_all.deb ]]; then
wget -o /dev/null \
-O /opt/cockpit/cockpit-docker_215-1~ubuntu19.10.1_all.deb \
https://launchpad.net/ubuntu/+source/cockpit/215-1~ubuntu19.10.1/+build/18889196/+files/cockpit-docker_215-1~ubuntu19.10.1_all.deb
fi
# Install all apt deps
[[ ! -z ${apt_deps_list} ]] && echo " ... Installing deps from apt ..." && apt-get install -y ${apt_deps_list} && echo
# Install all apt deps
[[ ! -z ${dpkg_deps_list} ]] && echo " ... Installing deps from local deb packages ..." && apt-get install -y ${dpkg_deps_list} && echo
# Install all pip3 deps
[[ ! -z ${pip3_deps_list} ]] && echo " ... Installing deps from pip ..." && pip3 install --no-cache-dir ${pip3_deps_list} && echo
# Configure network shares
echo " ... Installing SAMBA config ..."
mkdir -p /etc/samba
mv -f /etc/samba/smb.conf /etc/samba/smb.conf.original
echo "${_SAMBA_SHARE_CONFIG}" > /etc/samba/smb.conf
systemctl restart smbd
echo
echo " ... Installing NFS config ..."
mv -f /etc/exports /etc/exports.original
echo "${_NFS_SHARE_CONFIG}" > /etc/exports
exportfs -ra
systemctl restart nfs-kernel-server
echo
# Install updater bin
echo " ... Installing updater ..."
cat << EOF > /usr/local/bin/no-frills-nas-update
#!/bin/bash
# Add header
echo >> /var/log/no-frills-nas-update.log
echo "#############################################" >> /var/log/no-frills-nas-update.log
echo >> /var/log/no-frills-nas-update.log
echo "Running $(date)" >> /var/log/no-frills-nas-update.log
echo >> /var/log/no-frills-nas-update.log
wget -O - https://gist.githubusercontent.com/Josh5/7f05e8383d7c8a76e511fd1764b0de2c/raw/install.sh | bash | tee -a /var/log/no-frills-nas-update.log
echo >> /var/log/no-frills-nas-update.log
EOF
chmod +x /usr/local/bin/no-frills-nas-update
echo
# Update default docker-compose services
echo " ... Updating system Docker stack ..."
docker-compose -f /opt/NoFrillsNAS/default-docker-compose.yml down
wget -o /dev/null \
-O /opt/NoFrillsNAS/default-docker-compose.yml \
https://gist.githubusercontent.com/Josh5/7f05e8383d7c8a76e511fd1764b0de2c/raw/docker-compose.yml
sed -i "s|HOST_TIMEZONE|$(cat /etc/timezone)|" /opt/NoFrillsNAS/default-docker-compose.yml
docker-compose -f /opt/NoFrillsNAS/default-docker-compose.yml pull
docker-compose -f /opt/NoFrillsNAS/default-docker-compose.yml up -d
echo
echo "Done!"
echo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment