Your Batocera box has a disk that will eventually fill up. Your NAS has room. This guide moves whichever systems you choose onto a network share and symlinks them back into place, so Batocera never knows the difference — and covers the two things that reliably go wrong: file permissions that silently break saves, and a boot hook that changed between versions.
Batocera can put its entire /userdata on a network
share, but that's an all-or-nothing decision that puts your configuration, saves
and scraped artwork behind the network too. Symlinking individual system folders
is the more flexible approach: the big systems that eat hundreds of gigabytes live
on the NAS, while small, latency-sensitive ones stay on local disk, and the box
still boots and runs if the NAS is off.
The rule of thumb: disc-based systems go on the NAS — PS2, GameCube, Wii, Dreamcast, PSX, PS3 — because they're huge and stream large sequential reads that a gigabit network handles comfortably. Cartridge-era systems stay local — NES, SNES, Mega Drive, Game Boy, arcade — because they're small enough not to matter and they load thousands of tiny files when scraping artwork, which is exactly the workload networks are worst at.
nfs-kernel-server.root, default password linux
unless you've changed it. Everything below is run over SSH./media, and read the note about that in step 6 before you
pick anything clever.On a plain Linux NAS, create the directory and add an export line.
mkdir -p /srv/batocera
echo "/srv/batocera 192.168.1.20(rw,sync,no_subtree_check,no_root_squash,insecure)" >> /etc/exports
exportfs -ra
exportfs -vno_root_squash is the option that matters.
Batocera runs everything as root. By default NFS maps a
remote root user to nobody — so ROMs will read fine and
writes will fail. If you later move saves onto the share, they'll silently
stop saving. Read-only ROM folders survive squashing; anything writable does not.
On a NAS with a web interface, find the equivalent setting rather than editing
files by hand — Unraid calls it the NFS rule under Share Settings, Synology has a
"Squash" dropdown that must be set to No mapping, TrueNAS has a
"Maproot User" field to set to root.
exportfs -v on the NAS shows what the kernel is actually
enforcing. Check it now, and check it again after any future change to the share's
settings.Never automate a mount you haven't performed manually first.
mkdir -p /media/nas
mount -t nfs -o rw,soft,timeo=150,retrans=2 192.168.1.10:/srv/batocera /media/nas
mount | grep /media/nasYou should get a line back showing the mount and its negotiated options. If the
mount is refused with a protocol or access error, add
nfsvers=3 to the options — some NAS builds export v3
only, and others place v4 exports under a pseudo-root that changes the path.
About those options. soft means a stalled
request eventually returns an error instead of blocking forever; on a games machine
that's what you want, because a hard mount will freeze
the interface indefinitely if the NAS goes away.
timeo is in tenths of a second, so
timeo=150 is 15 seconds — with
retrans=2, a failing request gives up after about 30.
This takes ten seconds and catches the single most common misconfiguration.
touch /media/nas/.writetest && echo WRITE-OK || echo WRITE-FAILED
stat -c '%U:%G %a' /media/nas/.writetest
rm /media/nas/.writetesttouch does not
prove you aren't being squashed. If the parent directory is world-writable the
write succeeds anyway — and then every file your emulators create is owned by
nobody, which breaks in confusing ways later. The
stat output must say root:root.
If it says nobody, go back to step 1;
no_root_squash isn't in force.Copy, verify, and only then delete the originals.
rsync -aH --info=progress2 /userdata/roms/ps2/ /media/nas/ps2/
du -sh /userdata/roms/ps2 /media/nas/ps2The trailing slashes matter — /ps2/ on both sides
copies the contents into the target rather than nesting a folder inside it.
The du comparison should show matching totals.
For a large library, run the copy under nohup so it
survives the SSH session dropping. Batocera ships no
tmux or screen.
nohup rsync -aH --partial --info=progress2 /userdata/roms/ps2/ /media/nas/ps2/ > /userdata/romcopy.log 2>&1 &
tail -c 300 /userdata/romcopy.log
pgrep -a rsync || echo FINISHEDRename rather than delete, so you can reverse this in one command.
for SYS in ps2 gamecube wii dreamcast psx; do
[ -d "/media/nas/$SYS" ] || continue
mv "/userdata/roms/$SYS" "/userdata/roms/$SYS.local-backup"
ln -s "/media/nas/$SYS" "/userdata/roms/$SYS"
done
ls -la /userdata/roms | grep media/nasEach system's folder is now a symlink into the share. EmulationStation follows
them without knowing or caring. Once you've booted and confirmed the games are all
present, delete the .local-backup folders to reclaim
the space.
Check for symlinks whose target doesn't resolve — a folder that failed to copy shows up here rather than as a mysteriously empty system in the interface:
for d in /userdata/roms/*; do [ -L "$d" ] && [ ! -e "$d" ] && echo "BROKEN: $d"; doneThe same trick works for /userdata/saves, which is
handy if you play the same games on more than one machine. It also raises the
stakes considerably: saves are written constantly, so this only makes sense once
step 3 passed cleanly and the boot mount in step 6 is reliable.
mkdir -p /media/nas/saves
rsync -aH /userdata/saves/ /media/nas/saves/
mv /userdata/saves /userdata/saves.local-backup
ln -s /media/nas/saves /userdata/saves/media/nas still
exists as an empty directory, so the symlink resolves to a valid-looking empty path.
Games then write saves into local storage, and you end up with two diverging sets.
Some standalone emulators — RPCS3 and Dolphin among them — go further and regenerate
a blank configuration on top. This is why the boot script in the next step retries
rather than attempting the mount once.The part most guides get wrong, because Batocera changed it.
Batocera's root filesystem is a read-only squashfs image with a temporary
overlay on top. Anything written outside /userdata and
/boot is discarded at reboot, so
/etc/fstab is not an option — it won't survive. Batocera
provides its own hooks instead, and which one you use depends on your
version.
| Hook | Runs | Use for a network mount? |
|---|---|---|
| /boot/boot-custom.sh v32+ | First thing after init.d starts | No — the network stack isn't up yet |
| /boot/postshare.sh v35+ | After /userdata mounts, before the splash video and EmulationStation | Yes — this is the right one |
| /userdata/system/custom.sh deprecated since v38 | After EmulationStation has already launched | No — deprecated, and too late anyway |
| /userdata/system/services/<name> v43+ recommended | From S99userservices, after EmulationStation launches | Fallback only — see below |
custom.sh, that is very likely why your share isn't
mounting. It has been deprecated since v38, and on v43 we found it simply never
ran — the file sits there, correct and executable, and is never invoked. There is no
error message. The services system replaced it, but services start after
EmulationStation has scanned your ROM folders, so a share mounted there arrives too
late and every networked system shows up empty until you restart the interface.
postshare.sh runs before the scan, which is what you
actually want./boot is mounted read-only, so it has to be
remounted writable first — and set back afterwards.
mount -o remount,rw /boot
cat > /boot/postshare.sh <<'EOF'
#!/bin/sh
MNT=/media/nas
SRV=192.168.1.10:/srv/batocera
mkdir -p "$MNT"
n=0
while [ $n -lt 15 ]; do
grep -q " $MNT nfs" /proc/mounts && exit 0
mount -t nfs -o rw,soft,timeo=150,retrans=2 "$SRV" "$MNT" 2>/dev/null && exit 0
n=$((n+1))
sleep 2
done
mkdir -p /userdata/system/logs
echo "$(date) NAS mount FAILED" >> /userdata/system/logs/nas-mount.log
EOF
mount -o remount,ro /bootpostshare.sh runs. But this is the one Batocera
hook that does not execute in the background — the boot waits for it. An
uncapped while true loop here doesn't retry politely,
it hangs your console forever. Thirty seconds maximum, then give up and boot./proc/mounts, not
mountpoint. Batocera's BusyBox doesn't include
the mountpoint command. A guard written with it never
succeeds, so the loop runs its full duration on every boot even when the mount
worked./media
is a temporary filesystem — everything under it is gone after a reboot. If you find
your mount point missing, that's normal and expected, not something that deleted
it.logger output can vanish.
A file under /userdata always survives. Its
absence is your success signal.chmod needed. postshare.sh
is handed straight to the bash interpreter, so the executable bit is irrelevant —
which is just as well, since /boot is usually FAT and
can't store one.If postshare.sh doesn't fire on your version, use a
service instead. The filename must have no extension and contain only
letters, digits and underscores. Because services start after EmulationStation, add
an interface restart at the end or accept restarting it by hand once after boot.
mkdir -p /userdata/system/services
cat > /userdata/system/services/nasmount <<'EOF'
#!/bin/bash
MNT=/media/nas
SRV=192.168.1.10:/srv/batocera
case "$1" in
start)
mkdir -p "$MNT"
grep -q " $MNT nfs" /proc/mounts || \
mount -t nfs -o rw,soft,timeo=150,retrans=2 "$SRV" "$MNT"
;;
stop)
umount -l "$MNT" 2>/dev/null
;;
esac
EOF
chmod +x /userdata/system/services/nasmount
batocera-services enable nasmount
batocera-services list userCheck from the command line before you touch the interface.
mount | grep /media/nas
cat /userdata/system/logs/nas-mount.log 2>/dev/null
ls /userdata/roms/ps2 | headA mount line, no log file, and a listing of games is the clean result. If the log file exists, the script ran and the mount failed — check the NAS is awake and that the Batocera box still has the IP address in your export rule.
Then let EmulationStation load and confirm the networked systems show their games. A system that appears empty despite the folder listing correctly is a stale gamelist cache rather than a mount problem — update the game list for that system from the interface.
| Symptom | Likely cause |
|---|---|
| Share missing after every reboot, mounts fine by hand | Wrong boot hook for your version — you're almost certainly using custom.sh. Move to postshare.sh. |
| Mount point directory doesn't exist after reboot | Normal. /media is a temporary filesystem; the boot script must recreate it. |
| Games load, saves silently don't persist | Root squashing. Verify with exportfs -v on the NAS and the stat test in step 3. |
| Boot hangs at a black screen after adding the script | An uncapped retry loop in postshare.sh. It isn't backgrounded — the boot waits for it. |
| Script never runs and produces no error | Windows CRLF line endings, or the wrong filename. Both fail silently. |
mount.nfs: access denied by server | The export rule doesn't match the box's current IP, or your NAS needs nfsvers=3. |
| Interface freezes while scrolling a networked system | Usually artwork loading over the network. If it persists, check dmesg on the Batocera box — storage faults on local USB drives present identically and are easy to misattribute to the share. |
| Everything works until the NAS reboots, then hangs | You're on a hard mount. Use soft with a bounded timeo. |
Nothing here is one-way.
SYS=ps2
rm /userdata/roms/$SYS
mv /userdata/roms/$SYS.local-backup /userdata/roms/$SYSRemove /boot/postshare.sh the same way you created it
— remount /boot writable, delete, remount read-only. The
ROMs on the NAS are untouched by any of this.
man 5 exports on any Linux system remains the clearest explanation of squashing.