DEV Community

John
John

Posted on

How to Self-Host Samba in Docker Privately: SMB3 Encryption, No SMB1 and a Locked-Down Home Server Share

Yes, you can run Samba in Docker on a home server and keep it private, but the defaults alone will not get you there. Make SMB3 the minimum protocol, require encryption, run it on host networking with bound interfaces, and keep TCP port 445 off the public internet entirely. Do those four things and a containerised Samba share is a sound private file server. Skip any of them and you have a convenient network drive that leaks more than you think.

TL;DR:

  • Solo developer with one Linux box and a laptop (for example, a backend dev mounting a code and media archive from a MacBook): Run a maintained Samba image on host networking with server smb encrypt = required, because every modern macOS, Windows and Linux client supports SMB3.
  • Household with an old scanner or a Windows 7 machine that needs SMB1: Keep SMB1 off the main share and put the legacy device on its own isolated share or VLAN, because turning SMB1 back on weakens every connection to that server.
  • Remote worker who wants files away from home (for example, a contractor opening a project folder from a café): Connect over WireGuard or Tailscale and never forward port 445, because SMB was built for trusted LANs, not for the open internet.
  • Synology, TrueNAS or Unraid owner: Harden the Samba that ships with the NAS rather than adding a container, because two SMB servers on one host fight over ports and permissions.
  • Anyone who mostly wants browser or phone access: Choose a web-based file manager instead of Samba, because SMB clients on mobile are clunky and SMB is not designed to run over HTTPS.

The central tradeoff: every hardening step that shuts out attackers also shuts out older clients and costs some throughput, so you are exchanging compatibility and raw speed for confidentiality.


Table of contents


Why run Samba in Docker instead of installing it on the host?

Samba has been a standard Linux package for decades. apt install samba puts it on almost any Debian or Ubuntu box in under a minute. The reason to use a container is not speed. It is control. A container keeps the whole SMB stack, its config and its version in a few files you can read, pin and throw away. On a home server that also runs other services, that is the difference between a tidy share and a config spread across /etc.

Aspect Host install Docker container
Version control Tied to your distro's release cycle and backports Pinned by image tag, upgraded when you change one line
Config location /etc/samba/smb.conf plus system users in /etc/passwd One mounted smb.conf and a users file beside your compose file
Rollback Downgrading packages is awkward and often blocked Switch back to the previous tag and restart
Blast radius smbd runs as root with the whole filesystem visible Only the volumes you mount are visible to the process
Network discovery Works out of the box Needs host networking or extra work for NetBIOS and WS-Discovery

The last row is the honest cost. A container adds a layer you have to reason about, especially for networking and UID mapping, which later sections cover.

Where the container runs is a separate choice. A spare home server, a NAS, a self-managed VPS or Yundera can all host it. Yundera is a managed Personal Cloud Server, built on CasaOS, that runs self-hosted apps as Docker containers on a server dedicated to the user.


Which Samba Docker image should you trust with your files?

Your file server image runs a network daemon as root with access to your data. Pick it as carefully as you would pick a base OS. Popularity is not enough. Several of the most-pulled Samba images on Docker Hub became popular years ago and have not kept pace with upstream security releases.

Criteria and candidates worth checking:

  • dperson/samba: This was the default choice in older tutorials and still has a huge pull count, but it is no longer actively maintained. A stale image means old Samba code, and that is the wrong starting point for a privacy-focused setup.
  • crazymax/samba: Built on Alpine with a readable Dockerfile and a published changelog. Configuration is a mounted config.yml that generates smb.conf, which is convenient but adds a layer between you and the real config.
  • ghcr.io/servercontainers/samba: Configured through environment variables, and supports Time Machine shares for macOS clients. Read the entrypoint script before trusting it, because env-driven images sometimes apply defaults you did not ask for.
  • Your own image from debian:stable-slim: About ten lines of Dockerfile: install samba, copy your smb.conf, run smbd --foreground --no-process-group. You inherit Debian's security patches and see every line of configuration yourself.
  • Version check before you commit: Run docker run --rm <image> smbd --version and compare the result with the current release on samba.org. If the image is more than one minor release behind and has no recent commits, move on.
  • Tag discipline: Pin a specific tag or digest in your compose file instead of latest. That way an upstream push cannot silently change the binary serving your files.

Host or bridge networking: which one actually works for SMB?

Networking is where most containerised Samba setups go wrong. SMB clients expect to reach the server on TCP 445 at a real LAN address. Discovery, the feature that makes the server appear in Finder or Windows Explorer, depends on broadcast and multicast traffic that Docker's default bridge network does not forward.

Mode What works What breaks or costs you
Bridge with -p 445:445 Direct connections to smb://192.168.1.10 or \\192.168.1.10 NetBIOS broadcasts on UDP 137 and 138 and WS-Discovery on UDP 3702 stay inside the bridge, so the server does not appear in network browsing
Bridge on a remapped port such as 4445:445 Linux and macOS clients that accept a port in the URL Windows always connects on 445 and cannot be told to use another port
network_mode: host Everything, including discovery, plus Samba's interfaces and bind interfaces only settings The container shares the host's network stack, so port conflicts with a host Samba are fatal
macvlan with its own LAN IP Discovery and a separate address for the share The Docker host cannot reach the container by default, and some Wi-Fi access points reject the extra MAC address

For a private home share, host networking is usually the right answer. It lets you bind Samba to one interface, such as your LAN or WireGuard adapter, which a later section relies on. If you can live without discovery and type the address by hand, bridge mode with only 445 published is a smaller exposure.

Not every host lets you pick. On a NAS, a VPS or a platform such as Yundera, check which network modes are available before you design around one.


How do you disable SMB1 and set a safe minimum protocol?

SMB1 is the protocol behind the 2017 WannaCry outbreak. It has no modern encryption and weak integrity protection. Since Samba 4.11, the default server min protocol has been SMB2_02, so a fresh install no longer accepts SMB1. That default still lets in SMB 2.0 and 2.1, which cannot encrypt traffic at all. For a private share, set the minimum yourself instead of trusting a default that an image's entrypoint might override.

The settings and checks that matter:

  • server min protocol = SMB3: Put this in the [global] section of smb.conf. Clients now need at least SMB 3.0, the first dialect with built-in encryption. If every client is Windows 10 or newer, you can go further with SMB3_11, which adds pre-authentication integrity checks against downgrade attacks.
  • Client compatibility check: Windows 8 and later, macOS since 10.10 and current Linux kernels with the cifs module all speak SMB3. Older Android file managers, cheap media players and network scanners often do not. List what you own before you change anything.
  • ntlm auth = ntlmv2-only: This has been the default since Samba 4.5, but set it explicitly anyway. It refuses the older NTLMv1 password hashes, which can be cracked quickly.
  • Verify the effective value: Run docker exec samba testparm -sv | grep -i protocol to see what Samba actually loaded, defaults included, not just what you typed.
  • Watch live sessions: docker exec samba smbstatus lists each connected client with its negotiated dialect, so a stray SMB2 session stands out at once.
  • Legacy devices: If a scanner truly needs SMB1, do not lower the global minimum for it. Give it a separate, isolated Samba instance with write access to a single drop folder.

SMB3 encryption and signing: what they protect and what they cost

Encryption and signing protect data in transit, between the client and smbd. They do nothing for data at rest on the disk. Since Samba 4.15, encryption is set with server smb encrypt, which replaced the older smb encrypt parameter. The modern dialect SMB 3.1.1 negotiates AES-128-GCM by default, and Samba 4.15 also added AES-256 ciphers.

Setting What it protects What it costs
server signing = auto with no encryption Nothing on most clients, because signing is only used if the client insists No overhead, and file names and contents cross the network in plain text
server signing = mandatory Integrity: an attacker on the path cannot alter or replay packets A hash for every packet, light on modern CPUs
server smb encrypt = desired Confidentiality for clients that support SMB3 encryption, plain text for the rest Silent fallback, so you never quite know which sessions are protected
server smb encrypt = required Confidentiality and integrity for every session, file names included SMB2 clients are refused, plus the CPU cost of AES on every read and write

Choose required for a private share. Encrypted sessions are integrity-protected too, so you do not need mandatory signing on top. Set it as well only if you keep some unencrypted shares for legacy clients.

The cost is CPU, not bandwidth. On x86 machines with AES-NI and recent ARM chips with crypto extensions, the overhead is usually small next to disk and gigabit network limits. On older low-power boards without hardware AES, encryption can become the bottleneck. Measure it yourself: copy a large file with smbclient before and after the change, and watch smbd in top while it runs.


A hardened smb.conf, explained line by line

Protocol and encryption settings are only part of the job. A private share also needs to refuse guests, stop advertising itself and limit what each user can see. These lines sit beside the minimum protocol and encryption settings already covered. Every one is a standard Samba parameter you can confirm with testparm.

The lines that do the remaining work:

  • server role = standalone server: Tells Samba it is not a domain member or controller, so it authenticates users only against its own local password database (passdb.tdb).
  • map to guest = Never and restrict anonymous = 2: Wrong passwords fail instead of falling back to a guest session, and anonymous clients cannot list users or shares.
  • disable netbios = yes and smb ports = 445: Turns off the legacy NetBIOS name service and stops listening on TCP 139. You lose browsing on very old clients, and the server announces itself to less of the network.
  • load printers = no, printcap name = /dev/null, disable spoolss = yes: A file server does not need the print spooler, and dropping it removes code that is still reachable over the network.
  • access based share enum = yes: Users only see shares they are allowed to open, so the share list stops revealing what else lives on the box.
  • valid users = @family on each share: Access goes to a named group, not to every account Samba knows about, and new accounts get no access until you add them.
  • create mask = 0640 and directory mask = 0750: New files cannot be read by other users and are never executable, whatever the client asks for.
  • guest ok = no and browseable = no on sensitive shares: No guest access at all, and the share stays out of browse lists even for users who are allowed to open it.

Mapping users, UIDs and permissions between the container and the host

Samba checks two things for every file operation: the Samba password, and the Linux permissions of the account behind it. Inside a container, that account has a numeric UID that must line up with the owner of the files on the host. If the numbers do not match, you get "access denied" on files you own, or files owned by an anonymous UID nobody recognises.

How to keep identities consistent:

  • Match UIDs explicitly: If your host files belong to UID 1000, create the container user with that same number, for example useradd -u 1000 -M -s /usr/sbin/nologin alice, then run smbpasswd -a alice. The nologin shell means the account can use Samba but cannot get a shell.
  • Persist the password database: Samba stores hashed passwords in passdb.tdb, usually under /var/lib/samba/private. Mount that directory as a volume, or every recreated container forgets its users and some images quietly regenerate them from environment variables.
  • Be careful with force user: It makes every client write as one Linux account. That fixes permission headaches, but every file then belongs to the same owner and per-person accountability disappears.
  • Account for user namespace remapping: With rootless Docker or userns-remap, UID 1000 in the container becomes a high UID on the host, often in the 100000 range. Set ownership on the host to the shifted value, or the share will look empty to clients.
  • Mount read-only where you can: A bind mount with :ro, such as /srv/media:/share/media:ro, enforces read-only at the kernel level, so no Samba setting can override it.
  • Keep Windows ACLs on disk: vfs objects = acl_xattr stores NTFS-style permissions in extended attributes, which needs a filesystem with xattr support such as ext4, XFS or Btrfs.

Should you ever expose port 445, or use WireGuard and Tailscale instead?

No. Do not forward TCP 445 to a home server. SMB was designed for trusted local networks, and every serious SMB flaw of the last decade was first exploited at scale against machines that answered on 445 from the internet. Internet-wide scanners probe that port continuously, and many residential ISPs block it outright for that reason. Encryption and SMB3 do not change this. They protect the traffic, not the code that parses packets before authentication.

Option What the internet sees Tradeoffs
Port forward TCP 445 A live SMB service anyone can probe and brute-force Fails on ISPs that block 445, and one pre-auth bug exposes your files
WireGuard on your router or server One UDP port, 51820 by default, that stays silent to anything without a valid key You need a public IP or a forwarded port, so it fails behind carrier-grade NAT
Tailscale Nothing inbound, because clients reach each other through NAT traversal and relays Works behind CGNAT, but a third-party coordination server knows your device list and IPs, though not your file traffic
Headscale with Tailscale clients Only your own coordination server, usually behind HTTPS You run and patch one more service yourself
SSH tunnel with ssh -L 4445:localhost:445 Only SSH Windows cannot use a non-standard SMB port, so this realistically suits Linux and macOS clients

For a solo home server, WireGuard is the cleanest privacy story when you have a public IP, and Tailscale is the practical choice when you do not. Either way, Samba should listen only on the LAN and VPN interfaces. The next section shows how to enforce that so a misconfigured router cannot undo it.


How do you restrict Samba with firewall rules, interface binding and host allow lists?

Treat network access as three separate layers. Samba decides which interfaces it listens on, Samba decides which addresses it will talk to, and the host firewall decides which packets arrive at all. Any one of them can be misconfigured by an image update or a router change. All three failing together is far less likely.

The three layers, plus the Docker trap:

  • interfaces = lo eth0 wg0 with bind interfaces only = yes: Samba only opens sockets on the named interfaces, so a new public interface on the host gets nothing. This needs host networking, because inside a bridge network the container only ever sees its own eth0.
  • hosts allow = 127.0.0.1 192.168.1.0/24 10.8.0.0/24 100.64.0.0/10 with hosts deny = 0.0.0.0/0: Connections from anywhere else are dropped before authentication starts. The last range is the address block Tailscale assigns, so remove it if you use plain WireGuard only.
  • Host firewall rule: With host networking, a normal rule such as ufw allow from 192.168.1.0/24 to any port 445 proto tcp, followed by a default deny, works as expected. The equivalent nftables rule on the input chain does the same job.
  • The Docker trap with published ports: In bridge mode, ports published with -p 445:445 are handled by Docker's own iptables rules, which ufw does not see. Your ufw deny will look correct and still not block the port. Put restrictions in the DOCKER-USER chain instead, or publish only to a specific address such as -p 192.168.1.10:445:445.
  • Verify the listening sockets: Run ss -tlnp | grep 445 on the host. Every line should show a LAN, VPN or loopback address, never 0.0.0.0 or [::] unless your firewall rules have been tested to cover them.

Audit logging with vfs_full_audit: knowing who opened, changed or deleted a file

Locking the door is half the job. The other half is knowing what happened behind it. Samba ships a module, vfs_full_audit, that records file operations together with the user and client address. On a home server that answers questions like "who deleted the tax folder" or "why did 4,000 files get renamed at 3 a.m.", which is often the first visible sign of ransomware on a mapped drive.

How to configure it without drowning in logs:

  • Enable it per share: Add vfs objects = full_audit to the share, alongside acl_xattr if you use it. Order matters, so list full_audit first so it sees each operation before other modules change it.
  • Log identity with %U, not %u: Use full_audit:prefix = %U|%I|%m|%S. %U is the name the client logged in with, while %u becomes the forced account if you set force user, which would hide who actually acted.
  • Choose operations deliberately: full_audit:success = mkdirat renameat unlinkat openat captures structure changes and opens. Avoid pread and pwrite, which log every chunk of every transfer and can produce gigabytes of noise.
  • Skip failures unless debugging: full_audit:failure = none keeps the log readable. Turn failures on only while you hunt a specific permission problem.
  • Get the log out of the container: By default the module writes to syslog, and most Samba images run no syslog daemon, so entries quietly vanish. Set full_audit:syslog = false to write to Samba's own log file, then mount /var/log/samba as a volume.
  • Treat the audit log as personal data: It records who opened what and when. Rotate it with logrotate, keep a defined retention period such as 90 days, and exclude it from any backup that leaves the house.

Snapshots, recycle bins and backups against ransomware on mapped drives

A mapped drive is the weak point in any SMB setup. If ransomware runs on a laptop with Z: mapped, it encrypts the share with that user's own valid credentials. No protocol setting stops that. What protects you is the ability to roll back, and a copy of the data the infected client can never reach.

Recovery layers, from fastest to most durable:

  • vfs_recycle for accidental deletes: With recycle:repository = .recycle/%U, recycle:keeptree = yes and recycle:versions = yes, deleted files move to a per-user folder instead of disappearing. It does nothing against ransomware that overwrites files in place, so treat it as an undo button, not a defence.
  • Filesystem snapshots with vfs_shadow_copy2: On Btrfs or ZFS, schedule read-only snapshots with btrbk, snapper or sanoid, for example hourly for 48 hours and daily for 30 days. Point shadow:snapdir and shadow:format at them and Windows clients see the snapshots in the Previous Versions tab.
  • Keep snapshots out of the container's reach: Take snapshots on the host, not from inside the Samba container, and never mount the snapshot directory read-write. A compromised client account must not be able to delete its own history.
  • Encrypted backups that leave the house: restic and borg encrypt on the client before upload, so the storage provider holds only ciphertext. That keeps the backup private even off-site.
  • Append-only backup targets: Use rest-server --append-only for restic or borg serve --append-only, so a stolen backup credential can add data but not delete earlier snapshots.
  • Test a restore on a schedule: Restore one folder each month to a scratch directory and compare checksums. A backup you have never restored is a hope, not a plan.

How do you prove the lockdown works with testparm, smbclient and nmap?

A hardened config you have not tested is only a set of intentions. Image entrypoints rewrite files, defaults shift between Samba releases, and a typo in smb.conf can switch a parameter off without warning. Run these checks after every change and after every image upgrade, from a separate machine on the LAN wherever possible.

The checks, with what a passing result looks like:

  • Syntax and effective values: docker exec samba testparm -s should end with "Loaded services file OK" and print no "Unknown parameter" warnings. An unknown parameter means Samba ignored that line, often because it was renamed in a newer release.
  • SMB1 is refused: smbclient -L //192.168.1.10 -U alice -m NT1 should fail at protocol negotiation. If you get a share list back, SMB1 is still live.
  • SMB2 is refused: Repeat with -m SMB2. With a minimum of SMB3 this must fail too. If it succeeds, unencrypted sessions are still possible.
  • Anonymous access is dead: smbclient -L //192.168.1.10 -N should return NT_STATUS_ACCESS_DENIED or a logon failure, not a list of shares.
  • Encryption is actually on: Connect from a real client, open a file, then run docker exec samba smbstatus. The encryption column for that session should show an AES cipher such as AES-128-GCM, not a dash.
  • The dialects on the wire: nmap -p445 --script smb-protocols,smb2-security-mode 192.168.1.10 should list only 3.x dialects and report that message signing is enabled.
  • Nothing answers from outside: From a phone hotspot or a cheap cloud VM, run nmap -Pn -p 139,445 <your public IP>. Both ports must show as filtered or closed. Anything else means a router rule is undoing your work.

Top comments (0)