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-modules holds the modules for Macs (below). It's only a "recommended" package, so --no-install-recommends would skip it unless it's named.
  • userdel -r ubuntu: Ubuntu's base image already has a user called ubuntu with 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 User sounds 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 says guest 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 = no is 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).
  • nmbd isn't watched. If it dies on its own, the container keeps running and Windows loses \\SERVER until 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.