All those who seem stupid, they are, and also so are half of those who do not, Quevedo

Continuing our Arch Linux post-installation journey, we now focus on system integration and refining the user experience. Having established a Samba share for universal compatibility, we now turn our attention to NFS (Network File System). This script configures a dedicated NFS server for high-performance, low-overhead file sharing specifically between Linux machines on the local network.
Beyond networking, we ensure our data storage is persistent and accessible by configuring /etc/fstab to automatically mount drives at boot. Finally, we polish the desktop environment by setting up MIME types and file associations. We configure xdg-open to correctly handle different file types—launching Neovim for code, MPV for media, and Zathura for PDFs—and create a custom desktop entry to seamlessly open terminal applications from the graphical file manager.
#!/usr/bin/env bash
# ── 15-nfs.sh ─────────────────────────────────────────────────────────────────
# Install and configure an NFS server.
#
# NFS vs Samba:
# NFS — Unix-native protocol. Fast, low overhead, perfect for Linux↔Linux.
# Samba — SMB/CIFS protocol. Works with Windows, macOS, and Linux.
# More overhead, but universal compatibility.
#
# Run: bash 15-nfs.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# ── Configuration ─────────────────────────────────────────────────────────────
NFS_SHARE="/mnt/data" # Directory to export
LAN_SUBNET="192.168.1.0/24" # We will only allow clients from our local network
SERVER_IP="192.168.1.46" # Our NFS server IP (update if different)
echo "── 15-nfs: installing and configuring NFS server..."
# ── Install ───────────────────────────────────────────────────────────────────
sudo pacman -S --needed --noconfirm nfs-utils
# ── Create the export directory ───────────────────────────────────────────────
sudo mkdir -p "$NFS_SHARE"
# VS Code Remote SSH (NixOS) connect to Arch as your_main_user (e.g., nmaximo7), typically UID 1000/1000
# Be careful, there may be a UID/GID mismatch
sudo chown -R 1000:1000 /mnt/data/
# 755: owner read/write/execute; others read/execute.
sudo chmod 755 "$NFS_SHARE"
echo " ✅ $NFS_SHARE created"
# ── /etc/exports ──────────────────────────────────────────────────────────────
# Each line: ()
#
# Export options explained:
# rw — clients can read and write
# sync — wait for writes to be committed to disk before replying.
# Slower than async but safer (no data loss on crash).
# no_subtree_check — disable subtree checking. Recommended modern default.
# Subtree checking causes problems when files are renamed.
# no_root_squash — let root on the client act as root on the server.
# Only use if you fully trust all LAN clients.
echo "── Writing /etc/exports..."
sudo tee /etc/exports > /dev/null << EXPORTS
# /etc/exports — NFS export table
# After editing: sudo exportfs -ra && sudo systemctl restart nfs-server
# Check active exports: sudo exportfs -v
# Share /mnt/data with all clients on 192.168.1.0/24
${NFS_SHARE} ${LAN_SUBNET}(rw,sync,no_subtree_check,no_root_squash)
# Read-only example (uncomment to add a read-only media share):
# /mnt/media ${LAN_SUBNET}(ro,sync,no_subtree_check,no_root_squash)
EXPORTS
echo " ✅ /etc/exports written"
# ── Apply exports ─────────────────────────────────────────────────────────────
# exportfs -ra: re-read /etc/exports and sync with the kernel NFS table
sudo exportfs -ra
echo " ✅ Exports applied"
# ── Enable and start NFS server ───────────────────────────────────────────────
# nfs-server.service is the main NFS daemon.
# rpcbind maps RPC program numbers to ports (required for NFSv3).
# NFSv4 only strictly needs nfs-server, but rpcbind is harmless to have.
sudo systemctl enable --now nfs-server rpcbind 2>/dev/null || \
sudo systemctl enable --now nfs-server
echo " ✅ nfs-server started and enabled"
# ── Firewall ──────────────────────────────────────────────────────────────────
if command -v ufw &>/dev/null; then
sudo ufw allow from "$LAN_SUBNET" to any port 2049
sudo ufw allow from "$LAN_SUBNET" to any port 111
echo " ✅ ufw: NFS ports opened for $LAN_SUBNET"
fi
# ── Verify ────────────────────────────────────────────────────────────────────
echo
echo "── Active exports:"
sudo exportfs -v
echo
echo "── NFS server status:"
sudo systemctl status nfs-server --no-pager | head -6
echo
echo "── Mount from another Linux machine:"
echo " sudo mount ${SERVER_IP}:${NFS_SHARE} /mnt/remote"
echo
echo "── Add to /etc/fstab for auto-mount at boot:"
echo " ${SERVER_IP}:${NFS_SHARE} /mnt/remote nfs defaults,_netdev 0 0"
echo
echo "✅ 15-nfs: done"
fileSystems."/mnt/data" = {
device = "192.168.1.46:/mnt/data";
fsType = "nfs";
options = [
"x-systemd.automount" # mount on first access, not at boot
"noauto" # do not mount during boot sequence
"nofail" # do not block boot even if the server is unreachable
"soft" # return error instead of hanging forever if server goes down
"timeo=30" # 3-second timeout before returning error (units: 0.1s)
"rw" # read-write (must also be rw in /etc/exports on the server)
"nfsvers=4"
"nolock" # important for VS Code — avoids lock-related hangs
];
};
# ── User — must have uid = 1000 to match Arch file ownership ─────────────────
users.groups.nmaximo7 = {
gid = 1000;
};
# Define a user account. Don't forget to set a password with ‘passwd’.
users.users.nmaximo7 = {
isNormalUser = true;
uid = 1000; # This must match Arch's nmaximo7 UID
group = "nmaximo7";
description = "nmaximo7";
extraGroups = [ "networkmanager" "wheel" "docker" "audio"];
shell = "${pkgs.zsh}/bin/zsh";
packages = with pkgs; [
# thunderbird
];
};
This script does two unrelated things:
#!/usr/bin/env bash
# ── 16-mime.sh ────────────────────────────────────────────────────────────────
# Set sensible MIME type defaults so xdg-open opens files with the right app.
# Also adds the /mnt/data drive to /etc/fstab for auto-mount at boot.
#
# Run: bash 16-mime.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# Determine the real user (even if script is run with sudo)
USERNAME="${SUDO_USER:-$(whoami)}"
# ══════════════════════════════════════════════════════════════════════════════
# A. /etc/fstab — mount /mnt/data at boot
# ══════════════════════════════════════════════════════════════════════════════
#
# The drive UUID is stable across reboots and device name changes.
# ext4 defaults means rw, suid, dev, exec, auto, nouser, async.
# The two trailing zeros: first = dump (0 = skip or do not dump),
# second = 2 (check at boot after the root partition, usually 1 for /).
#
DATA_UUID="ca04070e-a366-424b-8fa3-c9960c9fbdae"
DATA_MOUNT="/mnt/data"
FSTAB_LINE="UUID=${DATA_UUID} ${DATA_MOUNT} ext4 defaults 0 2"
echo "── A. Adding /mnt/data to /etc/fstab..."
sudo mkdir -p "$DATA_MOUNT"
if grep -q "$DATA_UUID" /etc/fstab; then
echo " ✅ /mnt/data already in /etc/fstab — skipping"
else
echo "" | sudo tee -a /etc/fstab > /dev/null
echo "# /mnt/data — data drive (added by 16-mime.sh)" | sudo tee -a /etc/fstab > /dev/null
echo "$FSTAB_LINE" | sudo tee -a /etc/fstab > /dev/null
echo " ✅ Added to /etc/fstab: $FSTAB_LINE"
fi
# Mount immediately without rebooting
sudo mount -a 2>/dev/null && echo " ✅ Drive mounted at $DATA_MOUNT" || echo " ⚠️ mount -a failed — drive may not be connected yet (will mount at boot)"
# ══════════════════════════════════════════════════════════════════════════════
# B. MIME TYPE DEFAULTS
# ══════════════════════════════════════════════════════════════════════════════
#
# xdg-open reads or consults ~/.config/mimeapps.list to decide which desktop application to launch
# for a given file type. Without this file, xdg-open either does nothing
# or launches the wrong app.
#
# Format: mime/type=app.desktop
# Find .desktop files in: /usr/share/applications/ and ~/.local/share/applications/
#
# How to find a mime type:
# file --mime-type myfile.pdf → application/pdf
# xdg-mime query filetype myfile → same thing
#
# How to find a .desktop file name:
# ls /usr/share/applications/ | grep -i firefox
#
echo "── B. Setting MIME type defaults..."
# Ensure the user's config directories exist
mkdir -p "$HOME/.config"
cat > "$HOME/.config/mimeapps.list" << 'MIMEAPPS'
# ── ~/.config/mimeapps.list ───────────────────────────────────────────────────
# Default application associations for xdg-open.
# Format: mime/type=application.desktop
# Multiple apps: mime/type=first.desktop;second.desktop;
# xdg-open uses the first one that exists on the system.
# Edit this file or use: xdg-mime default app.desktop mime/type
# ─────────────────────────────────────────────────────────────────────────────
[Default Applications]
# ── Directories ───────────────────────────────────────────────────────────────
# Directories → thunar.desktop
inode/directory=thunar.desktop
# ── Web / URLs ────────────────────────────────────────────────────────────────
# Web links → firefox.desktop
x-scheme-handler/http=firefox.desktop
x-scheme-handler/https=firefox.desktop
x-scheme-handler/ftp=firefox.desktop
x-scheme-handler/chrome=firefox.desktop
text/html=firefox.desktop
application/xhtml+xml=firefox.desktop
# ── Text and code (Neovim via Alacritty) ─────────────────────────────────────
# Text files → nvim-open.desktop (custom)
# nvim-open.desktop is created below — wraps `alacritty -e nvim %f`
text/plain=nvim-open.desktop
text/markdown=nvim-open.desktop
text/x-shellscript=nvim-open.desktop
text/x-python=nvim-open.desktop
text/x-lua=nvim-open.desktop
text/x-csrc=nvim-open.desktop
text/x-chdr=nvim-open.desktop
text/css=nvim-open.desktop
text/javascript=nvim-open.desktop
application/json=nvim-open.desktop
application/x-shellscript=nvim-open.desktop
application/xml=nvim-open.desktop
application/toml=nvim-open.desktop
# ── Documents ─────────────────────────────────────────────────────────────────
# PDFs → zathura.desktop
application/pdf=org.pwmt.zathura.desktop
application/vnd.oasis.opendocument.text=soffice.desktop
application/msword=soffice.desktop
application/vnd.openxmlformats-officedocument.wordprocessingml.document=soffice.desktop
application/vnd.ms-excel=soffice.desktop
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet=soffice.desktop
# ── Images ────────────────────────────────────────────────────────────────────
# Images → imv.desktop
image/png=imv.desktop
image/jpeg=imv.desktop
image/gif=imv.desktop
image/webp=imv.desktop
image/svg+xml=imv.desktop
image/bmp=imv.desktop
image/tiff=imv.desktop
# ── Video ─────────────────────────────────────────────────────────────────────
# Video/audio → mpv.desktop
video/mp4=mpv.desktop
video/mkv=mpv.desktop
video/x-matroska=mpv.desktop
video/webm=mpv.desktop
video/avi=mpv.desktop
video/quicktime=mpv.desktop
video/x-msvideo=mpv.desktop
# ── Audio ─────────────────────────────────────────────────────────────────────
audio/mpeg=mpv.desktop
audio/flac=mpv.desktop
audio/ogg=mpv.desktop
audio/wav=mpv.desktop
audio/x-wav=mpv.desktop
audio/mp4=mpv.desktop
audio/aac=mpv.desktop
# ── Archives ──────────────────────────────────────────────────────────────────
# Archives → thunar.desktop
application/zip=thunar.desktop
application/x-tar=thunar.desktop
application/x-7z-compressed=thunar.desktop
application/gzip=thunar.desktop
application/x-bzip2=thunar.desktop
application/x-rar=thunar.desktop
# ── Email ─────────────────────────────────────────────────────────────────────
x-scheme-handler/mailto=firefox.desktop
# ── Torrent ───────────────────────────────────────────────────────────────────
# x-scheme-handler/magnet=transmission-gtk.desktop
# application/x-bittorrent=transmission-gtk.desktop
[Added Associations]
text/plain=nvim-open.desktop;
inode/directory=thunar.desktop;
image/png=imv.desktop;
image/jpeg=imv.desktop;
video/mp4=mpv.desktop;
audio/mpeg=mpv.desktop;
application/pdf=org.pwmt.zathura.desktop;
MIMEAPPS
echo " ✅ ~/.config/mimeapps.list written"
# ── Create nvim-open.desktop ──────────────────────────────────────────────────
# xdg-open can't launch terminal apps directly — it needs a .desktop file
# that specifies Terminal=true or wraps the command in a terminal emulator.
# We create a custom .desktop that opens files in Neovim inside Alacritty.
mkdir -p "$HOME/.local/share/applications"
cat > "$HOME/.local/share/applications/nvim-open.desktop" << 'NVIMDESKTOP'
[Desktop Entry]
Name=Neovim (Alacritty)
Comment=Open file in Neovim inside Alacritty terminal
# %f = single file path passed by xdg-open
Exec=alacritty -e nvim %f
Type=Application
# Terminal=false because we launch our own terminal (Alacritty) explicitly
Terminal=false
Categories=TextEditor;Development;
MimeType=text/plain;text/markdown;text/x-shellscript;text/x-python;text/css;application/json;application/toml;
Icon=nvim
StartupNotify=false
NVIMDESKTOP
echo " ✅ nvim-open.desktop created"
# ── Update the desktop database ───────────────────────────────────────────────
# We do so xdg-open picks up the new mimeapps.list and .desktop files
if command -v update-desktop-database &>/dev/null; then
update-desktop-database "$HOME/.local/share/applications"
echo " ✅ Desktop database updated"
fi
# ── Install missing apps referenced in mimeapps.list ─────────────────────────
echo "── Installing apps referenced in mimeapps.list..."
sudo pacman -S --needed --noconfirm \
alacritty \
imv \ # image viewer (Wayland-native)
mpv \ # media player
thunar \ # file manager
zathura \ # PDF viewer (minimal, keyboard-driven)
zathura-pdf-mupdf # PDF backend for zathura
# LibreOffice (for Office documents) — large download, comment out if needed
# sudo pacman -S --needed --noconfirm libreoffice-fresh
echo
echo "── Verify a MIME association:"
echo " xdg-mime query default text/plain"
echo " xdg-mime query default inode/directory"
echo " xdg-mime query default image/png"
echo
echo "✅ 16-mime: done"
echo
echo "══════════════════════════════════════════════════════════"
echo " Full installation complete! Run: sudo reboot"
echo "══════════════════════════════════════════════════════════"