AD-Integrated Containerized Samba File Server
This repository provides a production-oriented Samba file server container that joins an existing Active Directory domain and exposes three SMB shares: Private, Data, and FSLogix.
Architecture
- Samba runs in ADS mode with
winbindidentity mapping. - Static shares:
\\server\Private->/data/private\\server\Data->/data/groups/data\\server\FSLogix->/data/fslogix
- FS_* groups are projected as folders inside the Data share (
/data/groups/data/<groupName>). - Data folder ACLs expand nested AD group membership recursively and detect group cycles.
- Group records are persisted in SQLite at
/state/shares.db. - Group folders are name-based while active and moved to archive on deactivation:
- active:
/data/groups/data/<groupName> - inactive/deleted groups:
/data/groups/archive/<groupName>
- active:
- Samba machine trust/key material is persisted in
/var/lib/sambato survive container recreation. - Container hostname is fixed (
SAMBA_HOSTNAME) to keep AD computer identity stable. - In bridge-mode Docker networking, startup can publish the host LAN IP in AD DNS with
AD_DNS_IP/AD_DNS_NAMEinstead of the container bridge IP. - NetBIOS name defaults to
ADSAMBAFSRVand is clamped to 15 characters (NETBIOS_NAMEoverride supported). - Setup prompts for well-known authorization groups by SID (
DOMAIN_USERS_SID,DOMAIN_ADMINS_SID) to avoid localized group names. FSLOGIX_GROUP_SIDcontrols who can access the default FSLogix share (defaults toDOMAIN_USERS_SID).- Startup resolves those SIDs to NSS group names via winbind, then uses those resolved groups in Samba
valid usersrules. - Share operations are audited with Samba
full_audit(connect, list, read, write, create, delete, rename) and written to Samba log files. - Optional remote backups run when
BACKUP_DESTINATIONis configured. - Private home creation skips well-known/service accounts by default (including
krbtgt,msol_*,FileShare_ServiceAcc). - Reconciliation is executed:
- once on startup
- every 5 minutes via cron
- Backup is executed:
- daily at
BACKUP_START_HOUR(default:2, i.e. 02:00)
- daily at
Data Folder Lifecycle
The reconciliation script (/app/reconcile_shares.py) enforces these rules:
- New matching
FS_*group -> insert DB row and create/data/groups/data/<groupName>. - Group rename while still matching
FS_*-> rename/update folder path. - Group removed or no longer matching
FS_*-> setisActive=0and move folder to/data/groups/archive/.... - Previously inactive group returns -> set
isActive=1, move back into/data/groups/data/....
SQLite State Database
Database path: /state/shares.db
Table schema:
CREATE TABLE shares (
objectGUID TEXT PRIMARY KEY,
samAccountName TEXT NOT NULL,
shareName TEXT NOT NULL,
path TEXT NOT NULL,
createdAt TIMESTAMP NOT NULL,
lastSeenAt TIMESTAMP NOT NULL,
isActive INTEGER NOT NULL
);
AD Requirements
- Existing AD DS domain reachable from the Docker host.
- Initial admin credentials with rights to create/reset
FileShare_ServiceAccountduring./setup. FileShare_ServiceAccountmust be allowed to join computers to the domain (net ads join) in your AD policy.- Dynamic group discovery primarily uses machine-account LDAP (
net ads search -P); join credentials are only used as a fallback LDAP bind path. - Group naming convention for Data folder eligibility:
FS_<Anything>
- Folder names use AD group display names (
displayName, thenname/cnfallback), not pre-2000 (sAMAccountName) names.
DNS Requirements
- Container must resolve AD DNS records (especially SRV records for domain controllers).
DOMAINshould resolve from inside the container.- Preferred setup: Docker host uses AD-integrated DNS or forwards to AD DNS.
- If Docker bridge networking is used, set
AD_DNS_IPto the Docker host LAN IP that clients should use for SMB, not the container172.xaddress. - When
AD_DNS_IPis set, domain join uses--no-dns-updates; each container startup unregisters/re-registersAD_DNS_NAME -> AD_DNS_IPin AD DNS using the machine account. ./setupand./redeployauto-refreshAD_DNS_IPfrom the host route whenAD_DNS_IP_AUTO=1.
Time Sync Requirements
Kerberos requires close time alignment.
- Docker host clock must be synchronized (NTP/chrony/systemd-timesyncd).
- AD domain controllers must also be time-synchronized.
- If join/authentication fails unexpectedly, check time skew first.
Repository Layout
Dockerfiledocker-compose.ymlsetup.env.exampleREADME.mdapp/init.shapp/reconcile_shares.pyapp/backup_to_destination.pyetc/samba/smb.conf
Setup
-
Run interactive setup:
./setup -
If
.envis missing, you will be prompted for:REALMWORKGROUPDOMAINAD_DNS_IP(host LAN IP to publish in AD DNS)- optional
AD_DNS_NAME(defaults toSAMBA_HOSTNAME.DOMAIN) - initial admin credentials (used once for provisioning)
DOMAIN_USERS_SIDDOMAIN_ADMINS_SID- optional
FSLOGIX_GROUP_SID(defaults toDOMAIN_USERS_SID) - optional
BACKUP_DESTINATION(empty disables backup) - optional
BACKUP_START_HOUR(0-23, default2) - optional
BACKUP_RETENTION_DAILY(default3) - optional
BACKUP_RETENTION_WEEKLY(default2) - optional
BACKUP_RETENTION_MONTHLY(default2) - optional
BACKUP_RETENTION_YEARLY(default1) - optional
BACKUP_LOG_FILE(default/var/log/backup.log) - optional
BACKUP_PROGRESS(auto,always, ornever; defaultauto) - optional
BACKUP_PROGRESS_INTERVAL_SECONDS(default10)
Optional:
SAMBA_HOSTNAME(defaults toadsambafsrv)NETBIOS_NAME(defaults toADSAMBAFSRV, max 15 chars)
-
Setup behavior:
- creates or updates AD service account from desired name
FileShare_ServiceAccount - uses a valid AD
sAMAccountName(max 20 chars); default effective value isFileShare_ServiceAcc - always sets a long random password
- writes only service-account credentials to
.env(initial admin credentials are not stored) - writes
AD_DNS_IPso container restarts keep AD DNS pointed at the host LAN IP
- creates or updates AD service account from desired name
-
The setup script then starts the service with:
docker compose up -d -
After startup:
- container joins AD (idempotent)
- startup reconciliation runs
- cron runs reconciliation every 5 minutes
SMB Shares
Private
- Share:
\\server\Private - Root path:
/data/private - Per-user path:
/data/private/<samAccountName> - Script ensures user directories exist and assigns ownership through winbind identity resolution.
- Root
/data/privateis enforced read/execute-only (0555) to prevent folder creation directly under\\server\Private. - SMB-side ACL changes on
\\server\Privateare blocked (nt acl support = no). - Auto-creation skips well-known/service/non-login accounts (disabled, locked, or expired).
- Each private user tree is reconciled recursively to homogeneous permissions (dirs
0700, files0600, user/admin ACLs). - Permissions:
- owner user: full control
- Domain Admins: ACL full control
- mode:
700
hide unreadable = yes+ ACLs enforce that users only see their own folder.
Data
- Share:
\\server\Data - Path:
/data/groups/data - Contains one folder per active
FS_*AD group. - Root is discoverable as one share, while access to each group folder is enforced via POSIX/ACL group permissions.
FS_*groups may contain other AD groups; reconciliation recursively grants ACLs to nested groups and logs detected cycles.- Samba blocks SMB-side ACL edits on Data and forces new items to inherit the group folder owner, group, mode, and default ACLs.
- Normal reconciliation refreshes each group folder root; recursive subtree repair runs only when the resolved ACL signature changes, is missing, or
REPAIR_DATA_ACLS=1is set. - Dot-prefixed group folder names are allowed and are not hidden over SMB.
- No guest access.
FSLogix
- Share:
\\server\FSLogix - Path:
/data/fslogix - Access for authenticated users in configurable
FSLOGIX_GROUP_SID(default:DOMAIN_USERS_SID, resolved through winbind). - Semantics intentionally differ from
Data: only the share root is reconciled (03770+ ACL defaults), while user-created profile container folders/files are not recursively normalized. - Samba masks are profile-container oriented (
create mask = 0600,directory mask = 0700) so profile payload stays user-private by default.
Backups
-
Backups are enabled only if
BACKUP_DESTINATIONis non-empty. -
Each run creates a timestamped snapshot under
snapshots/YYYYMMDDTHHMMSSZat the destination. -
Backup job is scheduled daily at
BACKUP_START_HOURin container local time. -
Sources synced to destination on each run:
/data/private->data/private/data/groups->data/groups/data/fslogix->data/fslogix/state->state/var/lib/samba/private->samba/private
-
Retention policy env vars (defaults):
BACKUP_RETENTION_YEARLY=1BACKUP_RETENTION_MONTHLY=2BACKUP_RETENTION_WEEKLY=2BACKUP_RETENTION_DAILY=3
-
The backup script writes directly to
BACKUP_LOG_FILE(default:/var/log/backup.log). Cron does not redirect backup output into the logfile. -
Before uploading, the backup script measures all source files so it can report total upload progress.
-
Upload progress is logged per file every
BACKUP_PROGRESS_INTERVAL_SECONDSseconds and again when a file reaches 100%, including percentage and transferred/remaining bytes with auto-scaled units. -
BACKUP_PROGRESS=autoshows an interactive multi-line progress view only for TTY/manual runs. Current file uploads are shown as separate rows, capped at 12 rows, with the total progress row at the bottom. Usealwaysto force it orneverto suppress the bar. File and total progress are still logged. -
Rclone-backed destinations cap concurrent file transfers at 12. Rsync remains single-streamed by rsync itself.
-
Retention logic:
- daily: newest N snapshots
- weekly: newest N snapshots created on week start (Monday)
- monthly: newest N snapshots created on day 1
- yearly: newest N snapshots created on Jan 1
- snapshots selected by any tier are retained; all others are pruned
-
Supported destination schemes:
rsync://user:pass@host/module/pathsmb://user:pass@host/share/path(domain user example:smb://DOMAIN%5Cuser:pass@host/share/path)davfs://user:pass@host/path(WebDAV over HTTPS)sftp://user:pass@host/path
-
Username/password components should be URL-encoded when they contain reserved characters (
@,:,/,\,%,#,?). -
Example:
BACKUP_DESTINATION=sftp://backupuser:StrongPassword@sftp.example.com/exports/samba BACKUP_START_HOUR=2 BACKUP_RETENTION_DAILY=3 BACKUP_RETENTION_WEEKLY=2 BACKUP_RETENTION_MONTHLY=2 BACKUP_RETENTION_YEARLY=1 BACKUP_LOG_FILE=/var/log/backup.log BACKUP_PROGRESS=auto BACKUP_PROGRESS_INTERVAL_SECONDS=10
Useful Commands
docker compose logs -f samba
docker compose exec samba python3 /app/reconcile_shares.py
docker compose exec samba sqlite3 /state/shares.db 'SELECT * FROM shares;'
docker compose exec samba testparm -s
docker compose exec samba sh -lc 'tail -n 200 /var/log/samba/log.*'
docker compose exec samba sh -lc 'tail -n 200 /var/log/backup.log'
Troubleshooting
Domain join fails
-
Verify service account credentials in
.env. -
Verify DNS resolution from container:
docker compose exec samba getent hosts "$DOMAIN" -
Verify time sync on host and AD DCs.
-
Verify NetBIOS name length is <= 15:
docker compose exec samba testparm -s | grep -i 'netbios name'
AD DNS points at 172.x
-
This means Samba registered the container bridge IP.
-
Ensure
.envcontainsAD_DNS_IP=<host LAN IP>andAD_DNS_NAME=<server FQDN>. -
Recreate/restart the container so startup re-registers AD DNS:
docker compose up -d --force-recreate samba
Winbind user/group resolution fails
-
Check trust:
docker compose exec samba wbinfo -t -
List users/groups:
docker compose exec samba wbinfo -u docker compose exec samba wbinfo -g
Data folders not appearing
-
Confirm AD groups match
FS_*. -
Run manual reconciliation and inspect logs:
docker compose exec samba python3 /app/reconcile_shares.py docker compose exec samba tail -n 100 /var/log/reconcile.log
Nested Data group access fails
-
Check reconciliation logs for detected group cycles or unresolved nested members.
-
Verify winbind can resolve every nested group to a local GID:
docker compose exec samba getent group 'EXAMPLE\NestedGroup' docker compose exec samba id 'EXAMPLE\alice' docker compose exec samba getfacl /data/groups/data/<groupName>
Data folder permissions are incorrect
-
Normal reconciliation avoids walking every file when the resolved ACL signature is unchanged.
-
Force a recursive repair for all active Data group folders with:
docker compose exec samba sh -lc 'REPAIR_DATA_ACLS=1 python3 /app/reconcile_shares.py' -
After a successful repair, later cron runs return to root-only Data ACL refreshes unless group ACLs change again.
acl_xattr.so or full_audit.so module load error
-
If logs show
Error loading module .../vfs/acl_xattr.so(orfull_audit.so), your running image is missing Samba VFS modules. -
Rebuild and restart with the updated image:
docker compose down docker compose up -d --build -
Verify modules exist:
docker compose exec samba sh -lc 'mods="$(smbd -b | sed -n "s/^ *MODULESDIR: //p" | head -n1)/vfs"; ls -1 "$mods"/acl_xattr.so "$mods"/full_audit.so'
Permissions in Private share are incorrect
-
Re-run reconciliation to rebuild private directories and ACLs:
docker compose exec samba python3 /app/reconcile_shares.py -
Check identity resolution for a user:
docker compose exec samba id 'EXAMPLE\\alice'
Notes
- User data is never automatically deleted.
- Inactive/deleted FS_* groups are moved to
/data/groups/archive. - Data and state survive container restarts via named Docker volumes (
/data/*,/state,/var/lib/samba).