The home media server also shares two folders with the rest of the house: a Mac, a Windows PC and a phone open them like any network drive. That's Samba, and until November 3, 2026, it was one of the last services still installed straight on the host, outside the Docker Compose stack everything else runs in. This is how it moved into a container without anyone re-entering a password or losing the server, and the one thing that went wrong: Macs stopped seeing it, because Docker's AppArmor profile blocks the channel Samba uses to announce itself.
What it doesn't fix, up front: the container doesn't make Samba any safer to put on the internet (don't), it doesn't back up the shares, and one small file still has to live on the host. The details are at the end.
What Samba is, briefly
SMB (Server Message Block) is the file-sharing protocol Windows uses for network drives; macOS, iOS and Android speak it too. Samba is the open-source SMB server for Linux. A share is just a name, a folder and a few options, and every device in the house can open it without installing anything.
This server has two shares, each behind a login (no guest access):
| Share | Where | For |
|---|---|---|
samba-share |
An old 2 TB WD Green, about 53,000 hours powered on | Short-term scratch space between devices; nothing stays long |
private-share |
An 8 TB WD Red | Private files, used every day |
The media library is on other drives, and Jellyfin only mounts the media folders, read-only, so neither share ever shows up in Jellyfin.
The goal: nothing changes for the devices
Each device had a saved login and a saved server. For the move to be invisible, four things had to stay the same:
- The password. Samba keeps its own password database (
passdb.tdb), separate from the Linux login, so the container needs that file, readable by the same Samba. - The server's name and address, which saved connections point at.
- The share names.
- The file owner. Files written over SMB belong to a Linux user; the container's user needs the same uid (user id) as the host's, or new files come out owned by a stranger.
And one thing that's easy to miss: how each device finds the server in the first place.
How each device finds the server
"Finding" a server is a separate job from talking to it, and different devices do it differently:
| Device | Finds the server through | Which Samba piece answers |
|---|---|---|
| Mac | mDNS (Bonjour): the server in Finder's sidebar | Avahi, the host's mDNS service, which Samba asks to announce it |
| Phone | mDNS, in the file app's list of nearby servers | The same |
| Windows PC | NetBIOS name lookup: \\SERVER in Explorer |
nmbd, Samba's NetBIOS daemon (ports 137-138), and SMB on port 139 |
flowchart LR
mac([Mac, phone]) -->|mDNS: who offers SMB?| avahi[Avahi<br/>on the host]
win([Windows PC]) -->|NetBIOS: where is SERVER?| nmbd[nmbd]
mac -->|SMB, port 445| smbd[smbd]
win -->|SMB, port 445 or 139| smbd
smbd --> shares[(The two shares)]
My first version ran only smbd, the part that serves files, with NetBIOS switched off: modern
clients use port 445, and NetBIOS is old. Before deploying, I checked how the devices actually
connected, and Samba's own logs answered it. The old config wrote one log per client,
log file = /var/log/samba/log.%m, and the logs looked like this:
log.<ip-address>
log.<windows-pc-name>
log.<macbook-name>
log.fe80__... (phones and others, by IPv6 address)
%m is the client's NetBIOS name, and Samba's manual says it "is not available when Samba listens
on port 445, as clients no longer send this information." On 445, the log is named after the IP
address instead. So a log named after the Windows PC means it had connected through NetBIOS on
port 139. Switching NetBIOS off could have broken \\SERVER on that PC. The container now runs
nmbd alongside smbd, and listens on 139 as well as 445, as the host did.
The container
The image is built on the same Ubuntu release as the host (24.04), so it installs the same Samba version, 4.19.5. That removes any question about whether the copied password database can be read:
FROM ubuntu:noble-20260930.1
RUN set -e; \
apt-get update; \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends samba samba-vfs-modules; \
rm -rf /var/lib/apt/lists/*; \
mkdir -p /run/samba; \
userdel -r ubuntu; \
groupadd -g 1000 me; \
useradd -u 1000 -g 1000 -M -s /usr/sbin/nologin me
COPY start.sh /start.sh
ENTRYPOINT ["/bin/sh", "/start.sh"]
samba-vfs-modulesholds the modules for Macs (below). It's only a "recommended" package, so--no-install-recommendswould skip it unless it's named.userdel -r ubuntu: Ubuntu's base image already has a user calledubuntuwith uid 1000, the same uid the shares' owner has on the host. It goes, and the real owner is created with the host's uid and gid, so files written through the container belong to the same person as before.- The base image is pinned by date (
noble-20260930.1), like every image in the stack. Samba's security updates come from moving to a newer dated tag and rebuilding.
start.sh runs both daemons in the foreground, logging to stdout, so docker logs samba shows
both:
#!/bin/sh
nmbd --foreground --no-process-group --debug-stdout &
exec smbd --foreground --no-process-group --debug-stdout
If smbd exits, the container exits and Docker restarts it. (nmbd dying alone wouldn't restart
anything; a small gap I've accepted.)
In the Compose file, the container uses the host's network, so it answers on the server's own address and, because the container then shares the host's hostname, under the same server name:
samba:
build: ./samba
container_name: samba
network_mode: host
volumes:
- ./samba/smb.conf:/etc/samba/smb.conf:ro
- /home/me/.samba/private:/var/lib/samba/private
- /mnt/<drive-1-uuid>/samba-share:/shares/samba-share
- /mnt/<drive-2-uuid>/private-share:/shares/private-share
restart: unless-stopped
smb.conf sets no netbios name at all: it defaults to the hostname, which is the host's.
The config
The host's config was Ubuntu's default file with two shares added. The container's keeps what mattered and drops the rest (printers, the Unix password sync that only made sense on the host):
[global]
server role = standalone server
server string = %h server (Samba, Ubuntu)
map to guest = Bad User
multicast dns register = no
log level = 1
load printers = no
printing = bsd
printcap name = /dev/null
disable spoolss = yes
vfs objects = catia fruit streams_xattr
fruit:metadata = stream
fruit:model = MacSamba
fruit:posix_rename = yes
fruit:veto_appledouble = no
fruit:delete_empty_adfiles = yes
fruit:wipe_intentionally_left_blank_rfork = yes
[samba-share]
path = /shares/samba-share
read only = no
[private-share]
path = /shares/private-share
read only = no
map to guest = Bad Usersounds like guest access, but isn't: a login with a user name Samba doesn't know becomes the guest account, and a wrong password for a real user is still refused. Since no share saysguest ok = yes, the guest can't open either share anyway.- No SMB1. Samba's default minimum protocol is SMB2 (
server min protocol = SMB2_02), so the old, insecure SMB1 is off without saying so. multicast dns register = nois the fix for the surprise below.
Macs and ._ files
A Mac copying to a server that can't store its extra file data (Finder tags, resource forks)
writes it into a hidden sidecar file instead: photo.jpg gets a ._photo.jpg next to it, which
Windows users then see. Samba's fruit and streams_xattr modules store that data in the Linux
file's extended attributes, so the Mac stops writing sidecars, and fruit:convert_adouble
(on by default) converts old ones as they're read. catia maps characters macOS allows in file
names but Windows doesn't. Windows and phones ignore all of it.
There were only five ._ files on the scratch share, so this mattered less here than it does on a
share a Mac uses daily.
Testing before the switch
Everything that could be checked without stopping the host's Samba was checked first, on the server, with the image built under a test tag:
sudo docker build -t samba-test .
sudo docker run --rm -v $PWD/smb.conf:/etc/samba/smb.conf:ro --entrypoint testparm samba-test -s
sudo cp /var/lib/samba/private/passdb.tdb /var/lib/samba/private/secrets.tdb private/
sudo docker run --rm -v $PWD/smb.conf:/etc/samba/smb.conf:ro -v $PWD/private:/var/lib/samba/private \
--entrypoint pdbedit samba-test -L -v
testparm checks the config. pdbedit -L -v lists the users in the copied password database,
which proved the container could read it: the one user, with the password last set in
December 2024, so nobody's saved password would change. The test copy of the database was deleted
after.
The switch
With the image already built, the switch took seconds:
sudo systemctl disable --now smbd nmbd && sudo systemctl mask smbd nmbd
sudo mkdir -p ~/.samba/private && sudo chmod 700 ~/.samba/private
sudo cp -a /var/lib/samba/private/passdb.tdb /var/lib/samba/private/secrets.tdb ~/.samba/private/
sudo docker-compose -f jellyfin-docker-compose.yml up -d samba
The host's units are masked, not uninstalled: mask stops anything starting them again (a
package upgrade, say), and keeping the package makes going back two commands. secrets.tdb holds
the server's own identity; copying it too keeps everything Samba knew. The password database is
in the home directory now, which the nightly backup already covers.
Then the checks: both daemons in docker logs samba, ss -tulnp showing smbd on 445 and 139 and
nmbd on 137 and 138, and nmblookup SERVER answering with the server's address, so Windows could
find it.
The surprise: Macs stopped seeing it
Everything worked by name. But the server was gone from the Mac's network browser: avahi-browse
on the server listed the NAS and a Raspberry Pi, and not the server itself.
Samba normally announces itself by asking the host's Avahi, over D-Bus (Linux's system message
bus), to publish an _smb._tcp service. The kernel log said why that didn't happen:
apparmor="DENIED" operation="dbus_method_call" bus="system" path="/org/freedesktop/DBus"
interface="org.freedesktop.DBus" member="Hello" mask="send" label="docker-default"
AppArmor is the Linux security module that confines what a program may do. Docker puts every
container under its docker-default profile unless told otherwise, and Ubuntu's D-Bus checks
AppArmor before accepting a message. The profile doesn't allow D-Bus, so Samba's very first
message to the bus was refused. Mounting the bus's socket into the container, as I'd done, isn't
enough.
The options:
| Fix | Cost |
|---|---|
Run the container unconfined (security_opt: apparmor:unconfined) |
Removes AppArmor from a container that holds every file share, to fix an announcement |
| Write a custom AppArmor profile allowing D-Bus | A profile to maintain, for one message |
| Run Avahi inside the container | A second mDNS responder on the same host network, which Avahi itself warns makes mDNS unreliable |
| A static service file for the host's Avahi | One small file outside the container |
I took the last one. Avahi publishes any service described in /etc/avahi/services/, so the
announcement doesn't need Samba at all:
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">%h</name>
<service>
<type>_smb._tcp</type>
<port>445</port>
</service>
<service>
<type>_device-info._tcp</type>
<port>0</port>
<txt-record>model=MacSamba</txt-record>
</service>
</service-group>
%h becomes the hostname, _smb._tcp on 445 is the file service, and _device-info with
model=MacSamba (Samba's default for that field) picks the icon Finder shows for the server. Avahi watches that folder, so copying the file in was enough (its log: "Files changed,
reloading." and the service "successfully established" a second later). With the announcement
handled, multicast dns register = no stops Samba trying, and failing, on every start.
The Mac's dns-sd -B _smb._tcp listed the server again, and smbutil view from the Mac listed
both shares.
One visible difference: Samba announced the name in capitals, %h gives it in lower case, so
Finder's sidebar shows the same server in lower case.
The phone finds the server through the same mDNS announcement, and has connected without a problem
since. The Windows PC needed nothing: \\SERVER in Explorer found the server through the
container's nmbd, and the shares opened and worked as before.
Keeping it inside the house
SMB doesn't belong on the internet: it's a constant target, and a file share full of private files is the worst thing to expose. The router forwards only 80 and 443, to the web server, and neither 445 nor 139. Host networking means the container listens on every interface of the server, which is fine on a home network behind a router, and the reason the router's forwards matter.
What this doesn't fix
- The shares aren't backed up. The private share is about 6 TB and stays out of the nightly backup, by choice; the scratch share holds nothing for long. A dead drive loses its share.
- The scratch share is on an old drive. SMART says it's healthy, but SMART mostly catches drives that are already wearing out (reallocated sectors, read errors), not a motor or board that just stops. That's acceptable for scratch space and nothing else.
- One file lives on the host. The Avahi service file is outside the container, so a new server needs it copied in (the repo keeps it, and the setup notes say where).
nmbdisn't watched. If it dies on its own, the container keeps running and Windows loses\\SERVERuntil someone restarts it.- The container isn't a security boundary to lean on. It runs as root (Samba needs to switch to each user), with host networking, and it can write both shares. What it buys is a pinned version, config in the repo, and one less thing installed on the host.
Was it worth it? For one service, the move took an evening. What it bought is that the server is now: Ubuntu, Docker, the drive mounts, one Compose file, and a folder of settings to copy, and Samba is part of that instead of something to remember to install.