Topic page · SQL Server on Linux and Docker

SQL Server on Linux for Windows admins: The habits that change

Your check script asks for free space with xp_fixeddrives. SQL Server on Linux answers: drive C, 22,327 MB free. There is no drive C on this server. The number is still correct; it belongs to the file system under /var/opt/mssql.

Updated Nov 12, 2026 · approx. 21 min read

Part 2 · Windows adminsSQL query 3Lab SQL Server 2017–2025
On WindowsOn LinuxConfiguration Managermssql-confServices consolesystemctlC:\Program Files\…/opt/mssqlERRORLOG/var/opt/mssql/logWindows failover clusterPacemakerMaintenance plan wizardnot available

It is the same with many routines that are second nature on Windows. Some work unchanged, some return Windows answers to Linux questions, some lead nowhere without reporting an error. There is no Configuration Manager, and no maintenance plan wizard either. This page walks through the areas where Windows admins have to relearn, each with the comparison "On Windows · On Linux · Pitfall". What we say about Linux, we measured on SQL Server 2017, 2019, 2022 and 2025. What we say about Windows comes from Microsoft's documentation.

In 60 seconds

  • Files live under /var/opt/mssql, and C:\ exists anyway. SQL Server translates C:\ to /. A path like C:\var\opt\mssql\data\x.mdf works, C:\SQLData\x.mdf fails. SQL Server ignores case in file names on Linux too.
  • Everything belongs to the user mssql. A backup target without write permission for mssql ends with a Windows error message: Operating system error 5(Access is denied.).
  • mssql-conf replaces the Configuration Manager, systemctl replaces service management. In the container, environment variables beat the configuration file at every start. If you move master with mssql-conf without taking the files along, you start with an empty master.
  • Windows calls return Windows answers or nothing. Registry queries return NULL without T-SQL reporting an error, sys.dm_server_registry is empty, and sys.dm_server_services has different gaps in each version.
  • Maintenance plans do not exist against Linux instances. Ola Hallengren's maintenance solution ran in our lab on all four versions without errors; sp_Blitz did too, but misreads some Windows answers.
  • High availability without a Windows failover cluster: Pacemaker or no cluster manager at all, endpoints with certificates.

1 · Where does what live?

On WindowsOn LinuxPitfall
ProgramC:\Program Files\Microsoft SQL Server\…/opt/mssqlOne instance per host, named instances do not exist
Data and log of new databasesPaths from setup, often separate drivesboth /var/opt/mssql/dataData and log live together until you change it with mssql-conf
Default backup directoryPath from setup/var/opt/mssql/dataBackups land next to the database files
Errorlog…\MSSQL\Log\ERRORLOG/var/opt/mssql/log/errorlogText; the Agent log sqlagent.out is UTF-16
ConfigurationConfiguration Manager, registry, startup parameters/var/opt/mssql/mssql.confThe file only exists once you set something
Memory dumps…\MSSQL\Log/var/opt/mssql/log

Microsoft does not support symbolic links for data and log directories. Custom directories for data, log and backups, on the other hand, are common; they need the right permissions (section 2).

C:\ exists anyway

On Linux, SQL Server runs on an abstraction layer that presents a Windows environment to it. This shows in paths. We tried on all four versions what happens with Windows paths:

StatementResult
CREATE DATABASE with file C:\SQLData\x.mdfError 5133, operating system error 2: the directory does not exist
CREATE DATABASE with file C:\var\opt\mssql\data\x.mdfworks: the file lives in /var/opt/mssql/data/, the catalog still shows the Windows path
BACKUP … TO DISK = 'C:\var\opt\mssql\data\x.bak'works, the backup history shows the Windows path
BACKUP … TO DISK = 'C:\Temp\x.bak'2017 (runs as root in the container): creates /Temp/x.bak; from 2019: error 3201, operating system error 5
BACKUP … TO DISK = '\var\opt\mssql\data\x.bak'Error 3038: invalid file name
BACKUP … TO DISK = 'x.bak' (without a path)lands in the default backup directory
RESTORE … WITH MOVE … TO 'D:\MSSQL\Data\x.mdf'Error 5133, operating system error 2

So C:\ stands for /, other drive letters have no equivalent. This helps when a script with a fixed C:\ path happens to point to an existing Linux path, and it confuses because the catalog and backup history afterwards show Windows paths for files that live on Linux. Query 3 at the end of the page finds such paths. Our recommendation: always write Linux paths in T-SQL. When restoring a Windows backup, all files belong on Linux paths via WITH MOVE; that is the topic of part 6.

Upper and lower case

Linux file systems distinguish Fall.mdf and fall.mdf. SQL Server does not, not even on Linux:

  • If Fall.mdf was already in the directory, SQL Server rejected a new database with fall.mdf (error 5170, the file already exists).
  • A backup named L8.BAK could be read with RESTORE … FROM DISK = '…/l8.bak'.

Tools outside SQL Server, such as a cleanup script in the shell, do see the exact spelling. If you write file names consistently in lowercase, you spare yourself both surprises.

2 · Who may do what?

On WindowsOn LinuxPitfall
Service accountService account or virtual account, rights via NTFSUser mssql without interactive sign-in; everything under /var/opt/mssql belongs to itMicrosoft does not support changing the owner of the SQL Server directories
Custom backup directoryNTFS permission for the service accountOwner or group mssql, write permissionWithout permission: Operating system error 5(Access is denied.)
New filesinherit NTFS permissionsbelong to mssql, mode 640 or 660Other users and tools need group permissions
Additional confinement–SELinux (Red Hat) with package mssql-server-selinuxCustom data paths need the matching SELinux type

You set up a custom backup directory like this:

sudo install -d -o mssql -g mssql -m 770 /srv/sqlbackup
sudo /opt/mssql/bin/mssql-conf set filelocation.defaultbackupdir /srv/sqlbackup
sudo systemctl restart mssql-server

Note the error message when the permission is missing: SQL Server reports it in Windows wording, Operating system error 5(Access is denied.), not as a Linux message. Anyone searching Linux logs for "Permission denied" will not find it.

SELinux: Without the package mssql-server-selinux, SQL Server runs on Red Hat without SELinux confinement. With the package, it runs in its own domain, and custom data paths must get the type mssql_db_t (semanage fcontext -a -t mssql_db_t "/srv/sqldata(/.*)?", then restorecon -R -v /srv/sqldata). This comes from Microsoft's documentation; we could not verify it in the container. We found no statement from Microsoft on AppArmor on Ubuntu.

In the container, SQL Server runs from 2019 as the user with ID 10001. Who must own the data directory there is described in part 3.

3 · Service and configuration

On WindowsOn LinuxPitfall
Control the serviceServices console, Configuration Manager`systemctl start\stop\restart\status mssql-server or T-SQL SHUTDOWN`There is no pause; restarting the Agent also restarts the engine
SettingsConfiguration Managermssql-conf, writes /var/opt/mssql/mssql.confAlmost every change needs a restart
Trace flags at startupStartup parameter -Tmssql-conf traceflag 3226 on
Port, TLSConfiguration Managermssql-conf set network.tcpport, network.tlscert, network.tlskeyCertificate files must belong to mssql
Memory limitmax server memoryadditionally memory.memorylimitmb for the whole process, default 80%Set both limits, they mean different things
SQL Server AgentService "SQL Server Agent"mssql-conf set sqlagent.enabled trueDisabled after installation
Location of masterStartup parameters -d, -lmssql-conf set filelocation.masterdatafile, …masterlogfileWithout moved files, a new, empty master is created (see below)

The SQL Server Configuration Manager cannot connect to a Linux instance. This is what the replacement looks like:

sudo /opt/mssql/bin/mssql-conf set sqlagent.enabled true
sudo /opt/mssql/bin/mssql-conf traceflag 3226 on
sudo /opt/mssql/bin/mssql-conf set memory.memorylimitmb 12288
sudo cat /var/opt/mssql/mssql.conf
sudo systemctl restart mssql-server

After every change, mssql-conf answers with SQL Server needs to be restarted in order to apply this setting. In the 2022 and 2025 images it additionally points to sp_reload_mssqlconf, which can apply some of the settings without a restart; we did not check that.

Moving master: Caution

The safe procedure: stop the service, move the files master.mdf and mastlog.ldf to the new location, check that mssql owns them, only then set mssql-conf and start.

In the container: Variables win

In the container, you configure SQL Server with environment variables such as MSSQL_AGENT_ENABLED or MSSQL_MEMORY_LIMIT_MB. According to Microsoft they apply at setup; afterwards you are supposed to use mssql-conf. We combined both and restarted the container:

VariableSetting in mssql.confafter the restart (2017–2025)
MSSQL_AGENT_ENABLED=falsesqlagent.enabled = trueAgent off
MSSQL_AGENT_ENABLED=truesqlagent.enabled = falseAgent on
MSSQL_MEMORY_LIMIT_MB=2000memory.memorylimitmb = 30002,000 MB

The variable won on all versions at every start. For the memory limit, this precedence is also in the documentation; for the Agent we only saw it in the lab. Therefore define a setting in the container in exactly one place: either as a variable in the container definition or in a mounted mssql.conf, not in both.

Two more observations from the container:

  • In the SQL Server 2019 image, mssql-conf refuses to run for the container user; it has to run as root. Once root has written the file, later calls as the container user fail with Permission denied to modify SQL Server configuration. and still end with exit code 0. A script that only checks the exit code considers the change done.
  • systemctl does not exist in the container. The container is restarted; how to shut it down cleanly in the process is shown in part 3.

Reading the errorlog

On WindowsOn LinuxPitfall
Engine errorlogFile ERRORLOG, sp_readerrorlogFile /var/opt/mssql/log/errorlog, sp_readerrorlogThe file is text, although the documentation says UTF-16
Agent logFile SQLAGENT.OUTFile /var/opt/mssql/log/sqlagent.outUTF-16 with byte order mark
Service eventsEvent Viewersystemctl status mssql-server; in the container docker logsMicrosoft does not mention journalctl
Number of errorlogs keptSSMS, registrycannot be changedsee below

In all images we checked, errorlog began with plain text, sqlagent.out with the bytes ff fe, the marker for UTF-16 little endian. So cat and grep work directly for the errorlog, and for the Agent log after a conversion:

sudo iconv -f UTF-16LE -t UTF-8 /var/opt/mssql/log/sqlagent.out | tail -20

The number of retained errorlogs cannot be changed in SSMS against Linux, Microsoft says. The script SSMS runs for this on Windows writes a registry value. On Linux this call ran through without an error message in the lab and changed nothing.

4 · Storage, volumes, shares

On WindowsOn LinuxPitfall
File systemNTFS, ReFSXFS or ext4, NFS from version 4.2Other file systems are not supported
Backup to a shareBACKUP … TO DISK = '\\server\share\…'Mount the share as a directory (CIFS), then use the Linux pathUNC paths in T-SQL only via mssql-conf set uncmapping
Instant File InitializationRight "Perform volume maintenance tasks" for the service accounteffective for data files without a rightSome tools report it as disabled
Lock Pages in MemoryRight "Lock pages in memory"no documented equivalentAll images we checked reported the memory model CONVENTIONAL

Backups to a Windows share: On Linux, SQL Server does not address the share directly but writes to a directory under which the share is mounted. Microsoft shows this in the guides for failover cluster instances and log shipping; we found no dedicated guide for backups to shares. Following the pattern of the log shipping guide, with write permission:

sudo apt-get install cifs-utils          # Red Hat: dnf install cifs-utils
sudo install -d -o mssql -g mssql -m 770 /var/opt/mssql/backup-share
# /etc/fstab, one line; credentials in a file readable only by root
//fileserver/sqlbackup  /var/opt/mssql/backup-share  cifs  credentials=/root/.sqlbackup-creds,vers=3.0,uid=mssql,gid=mssql  0  0

Afterwards, BACKUP DATABASE … TO DISK = '/var/opt/mssql/backup-share/…' writes to the share. Microsoft requires SMB 3.0 or newer. For signing in to the file server with Kerberos instead of a password file we found no guide from Microsoft, and we did not check the mount itself in the container.

Instant File Initialization means: SQL Server creates new data files without first filling them with zeros. On Windows the service account needs a right for this. On Linux this right does not exist, and we measured whether the shortcut works anyway:

2017201920222025
New 4 GB data file0.7 s0.8 s0.8 s0.7 s
New 4 GB log file11.8 s11.6 s11.6 s11.5 s

Data files are created without filling, log files are written with zeros as on Windows (trace flag 3004 showed this in the errorlog). The column instant_file_initialization_enabled in sys.dm_server_services shows this only on 2019 and 2025. On 2017 and 2022 the row for the engine is missing entirely, and tools that look there report IFI as disabled (section 7).

Lock Pages in Memory: For Linux we found neither an equivalent nor a statement from Microsoft. Microsoft's Linux recommendations cover Transparent Huge Pages and vm.swappiness instead; that is the topic of part 4.

5 · Sign-in and Active Directory

SQL logins work as on Windows. Windows logins against an Active Directory are possible but need setup that joining the domain takes care of on Windows:

  1. Join the Linux host to the domain (for example with realmd and SSSD).
  2. Create an AD user account for SQL Server, with AES encryption for Kerberos.
  3. Register the SPNs MSSQLSvc/<FQDN>:<port> and MSSQLSvc/<hostname>:<port> for this account.
  4. Create a keytab (adutil on Linux or ktpass on Windows), place it under /var/opt/mssql/secrets, owner mssql, mode 400.
  5. mssql-conf set network.privilegedadaccount <account> and network.kerberoskeytabfile <path>, restart.
  6. Logins as usual with CREATE LOGIN [DOMAIN\user] FROM WINDOWS.

Pitfalls according to Microsoft: Clients must use the fully qualified host name (FQDN), not localhost or the IP address. If the password of the service account changes, SQL Server needs a new keytab. Domain-local groups are not supported. We did not set this up in the lab.

Every Linux instance brings two Windows logins: BUILTIN\Administrators and NT AUTHORITY\NETWORK SERVICE, both sysadmin. The steps of Agent jobs run under the second; the job history shows Executed as user: NT AUTHORITY\NETWORK SERVICE. According to Microsoft you should not delete BUILTIN\Administrators. Security checks that list Windows logins find both.

6 · Availability groups without a Windows failover cluster

On WindowsOn LinuxPitfall
ClusterWindows failover cluster (WSFC)Pacemaker (CLUSTER_TYPE = EXTERNAL) or none (NONE)Without Pacemaker only manual failover
EndpointsKerberos or certificatesCertificatesWindows authentication for endpoints does not exist
ListenerNetwork name in ADIP address as a cluster resource plus DNS entryShort outage during failover
Distributed transactionsMSDTCnot in availability groups

An availability group on Linux does not need an Active Directory. We built an availability group with CLUSTER_TYPE = NONE and certificate endpoints between Linux containers and ran eight manual failovers without data loss; the measurements are in our series FCI vs. Availability Groups. Part 5 of this series covers Pacemaker, fencing and automatic failover.

7 · Tools and scripts

On WindowsOn LinuxPitfall
SSMSfull scopeconnects as usualNo maintenance plans, no Data Collector, number of errorlogs cannot be changed
Configuration Manageryesdoes not connectmssql-conf (section 3)
Azure Data Studioretired since Feb 28, 2026likewiseSuccessor: VS Code with the MSSQL extension; Agent and classic administration stay in SSMS
sqlcmd, bcpyesyes, native
dbatoolsfull scopeCommands that use only T-SQLCommands using WMI or remoting target Windows (our interpretation from the source code)

What your scripts get back

Many check and maintenance scripts ask Windows: drives, registry, services. We ran the common calls on all four versions as sa:

worksworksworksworksworksworksworksworksworksworksworksworks/var/opt/…/var/opt/…/var/opt/…/var/opt/…partly NULLpartly NULLpartly NULLpartly NULLdrive Cdrive Cdrive Cdrive CNT x64NT x64NT x64NT x64Agent only“Stopped”Agent onlyno accountNULLLocalSystemNULLNULLNULL128NULLNULLno effectno effectno effectno effectno rowsno rowsno rowsno rows2017201920222025sys.dm_os_host_infoxp_dirtree '/var/opt/mssql'sp_readerrorlogxp_instance_regread 'BackupDirectory'sys.dm_os_volume_statsxp_fixeddrivesxp_msversys.dm_server_servicesxp_regread (service account)xp_instance_regread 'NumErrorLogs'xp_instance_regwritesys.dm_server_registryreturns Linux valuesWindows view or incompleteempty or no effect, without an error
worksworksworksworksworksworksworksworksworksworksworksworks/var/opt/…/var/opt/…/var/opt/…/var/opt/…partly NULLpartly NULLpartly NULLpartly NULLdrive Cdrive Cdrive Cdrive CNT x64NT x64NT x64NT x64Agent only“Stopped”Agent onlyno accountNULLLocalSystemNULLNULLNULL128NULLNULLno effectno effectno effectno effectno rowsno rowsno rowsno rows2017201920222025sys.dm_os_host_infoxp_dirtree '/var/opt/mssql'sp_readerrorlogxp_instance_regread 'BackupDirectory'sys.dm_os_volume_statsxp_fixeddrivesxp_msversys.dm_server_servicesxp_regread (service account)xp_instance_regread 'NumErrorLogs'xp_instance_regwritesys.dm_server_registryreturns Linux valuesWindows view or incompleteempty or no effect, without an error
What common Windows habits return on Linux, measured as sa on SQL Server 2017 to 2025 in containers. Details are in the table.
CallWhat it returns on Linux
xp_fixeddrivesone drive C with the free space of the file system under /var/opt/mssql
xp_dirtree 'C:\'a mix of Windows folders (binn, Windows, Program Files) and Linux entries (.dockerenv, run)
xp_dirtree '/var/opt/mssql'the directory contents, as expected
xp_regread (service account)NULL with the message RegQueryValueEx() returned error 2; only 2019 returned "LocalSystem"
xp_instance_regread (BackupDirectory)/var/opt/mssql/data
xp_instance_regread (other values)mostly NULL, without a T-SQL error
xp_instance_regwriteruns without an error, changes nothing
sys.dm_server_registryno rows
sys.dm_server_services2017 and 2022: only the Agent; 2019: engine as "Stopped" although it is running; 2025: engine and Agent. According to the DMV the Agent runs as "LocalSystem"
sys.dm_os_volume_statsSize and free space yes; mount point, volume name and file system type NULL
xp_msverPlatform "NT x64"
xp_readerrorlog, sp_readerrorlogwork
sys.dm_os_host_infoLinux and the distribution; the reliable way to detect the platform

The most dangerous row is the registry: a script that checks @@ERROR does not notice that it read nothing, and writes have no effect. Without sysadmin permissions, xp_fixeddrives, xp_dirtree and sys.dm_os_volume_stats returned empty results instead of errors. Our recommendation: scripts that are meant to run on both platforms should query sys.dm_os_host_info first and branch afterwards, as the two following projects do.

Maintenance without a maintenance plan: Ola Hallengren

The common replacement for maintenance plans is the SQL Server Maintenance Solution by Ola Hallengren. It detects Linux via sys.dm_os_host_info and adapts path separators and procedures. We installed and ran the version of Sep 12, 2026 with default settings on all four versions:

  • Installation without an error message. On Linux it creates ten Agent jobs, all with T-SQL steps. It does not create the job "Output File Cleanup", which on Windows deletes old output files with PowerShell; on Linux the installer picks only jobs with T-SQL steps. The Agent cannot run operating system commands or PowerShell on Linux anyway.
  • Backups, integrity checks, index maintenance ran without errors. Without @Directory, it backed up to the default backup directory under /var/opt/mssql/data/<server>/<database>/FULL/; SQL Server created the subdirectories itself during BACKUP. It deleted old backups with @CleanupTime.
  • The jobs ran successfully on 2022 and 2025 and wrote their output files to /var/opt/mssql/log. On 2017 and 2019 we could not start them through the Agent (see Method and limits).

What you should add yourself on Linux: nobody cleans up the output files of the jobs, and without @Directory backups sit next to the database files. Specify your own backup directory and delete old output files with a cron job or a T-SQL job step.

Diagnostics: sp_Blitz

sp_Blitz from the First Responder Kit supports Linux fully according to the project page, except for sp_DatabaseRestore. With version 8.34 of Jul 2, 2026 it ran without an error message on all four versions, as did sp_BlitzFirst. Some findings on Linux rest on the Windows answers from the table above:

Finding (original)Meaning on Linux
Drive C Spacethe free space under /var/opt/mssql, not of a drive C
Power Plan: "unknown power mode"cannot be determined on Linux; part 4 covers the CPU settings
Instant File Initialization Not Enabled (2017, 2022)does not apply, IFI works (section 4); the engine is missing from sys.dm_server_services
Invalid login defined with Windows Authentication: BUILTIN\Administratorslogin from the installation, not an error in your configuration
Sysadmins: NT AUTHORITY\NETWORK SERVICElikewise; Agent jobs run under this login

sp_Blitz detects the operating system and the container correctly. Read the other findings as usual, but check the rows on drives, power plan, IFI and Windows logins against this table before you act.

8 · Updates

On WindowsOn LinuxPitfall
Cumulative updatesInstaller of the CUPackage manager from the repository mssql-server-<version> (dnf/yum, apt-get, zypper)The repository fixes the major version
Without internet accessCopy the CU fileDownload the package file and install it locally
Back to an older versionUninstall the CUDowngrade the package, only within the major version
In the container–new image with a new tag, same data volumePart 3
sudo apt-get update && sudo apt-get install mssql-server     # Ubuntu
sudo dnf update mssql-server                                  # Red Hat

Profile: What does this instance have?

The following query summarizes the points on this page for a concrete instance: platform and paths, memory model, database files per directory with free space, and Windows leftovers in msdb, such as job steps with drive letters after a migration. Query 2 in part 3 covers the container limits and the Agent; Query 1 in part 1 shows which features missing on Linux an instance uses.

Query: Instance profile

Query 3: Instance profile · read-only · tested in the PSG lab, versions in the header
/* PSG Stack · SQL Server on Linux and Docker · Query 3 "Instance profile"   For Windows admins: where do data files and backups live, how much space is free there,   which memory model applies, and which Windows leftovers are still in msdb?    !!! WARNING — please read before running !!!   - Read-only. The query changes no data and no setting.   - Permissions: VIEW SERVER STATE and VIEW ANY DEFINITION for parts 1 and 2, read access to     msdb for part 3. If a permission is missing, the part returns no rows or says so in the     column note instead of showing false zeros.   - MAXDOP 1: every part runs serially and uses no additional cores.   - SQL Server 2017 and later. Also runs on Windows; the notes are written for Linux.     Tested in the PSG lab on 2017, 2019, 2022 and 2025 in Linux containers, as sa and without     sysadmin (12 Nov 2026); not tested on Windows.   - Part 3 scans job steps and the backup history of the last 30 days. With a very large     history in msdb this can take a few seconds.*/SET NOCOUNT ON; DECLARE @sql nvarchar(max);DECLARE @linux bit = 0;IF OBJECT_ID(N'sys.dm_os_host_info') IS NOT NULL    SELECT @linux = CASE WHEN host_platform = N'Linux' THEN 1 ELSE 0 END FROM sys.dm_os_host_info; /* 1 · Platform, default paths, memory model */SET @sql = N'SELECT ' +  CASE WHEN OBJECT_ID(N'sys.dm_os_host_info') IS NULL THEN N'N''n/a'' AS platform, N''n/a'' AS distribution, N''n/a'' AS release'       ELSE N'h.host_platform AS platform, h.host_distribution AS distribution, h.host_release AS release' END + N',    CONVERT(nvarchar(20), SERVERPROPERTY(''ProductVersion'')) AS version,    CONVERT(nvarchar(260), SERVERPROPERTY(''InstanceDefaultDataPath'')) AS default_data_path,    CONVERT(nvarchar(260), SERVERPROPERTY(''InstanceDefaultLogPath'')) AS default_log_path,    ISNULL(CONVERT(nvarchar(260), SERVERPROPERTY(''InstanceDefaultBackupPath'')), N''n/a (before 2019)'') AS default_backup_path,    CONVERT(nvarchar(260), SERVERPROPERTY(''ErrorLogFileName'')) AS errorlog,    i.sql_memory_model_desc AS memory_model,    CASE i.sql_memory_model_desc WHEN N''CONVENTIONAL'' THEN N''no locked pages (Lock Pages in Memory not active)''                                 WHEN N''LOCK_PAGES'' THEN N''locked pages active''                                 WHEN N''LARGE_PAGES'' THEN N''large pages active'' END AS memory_model_meaningFROM sys.dm_os_sys_info AS i' +  CASE WHEN OBJECT_ID(N'sys.dm_os_host_info') IS NULL THEN N'' ELSE N' CROSS JOIN sys.dm_os_host_info AS h' END +  N' OPTION (MAXDOP 1);';EXEC sys.sp_executesql @sql; /* 2 · Database files per directory with free space   On Linux mount_point stays empty; size and free space are still reported.   A path with C:\ is possible on Linux: SQL Server then places the file under /   (C:\var\opt\mssql\data\x.mdf lives in /var/opt/mssql/data/x.mdf) and stores the Windows path. */IF HAS_PERMS_BY_NAME(NULL, NULL, N'VIEW ANY DEFINITION') = 1    SELECT        LEFT(f.physical_name, LEN(f.physical_name) - pos.p) AS directory,        f.type_desc AS file_type,        COUNT(*) AS files,        COUNT(DISTINCT f.database_id) AS databases,        CONVERT(decimal(12, 1), SUM(CONVERT(bigint, f.size)) * 8 / 1048576.0) AS size_gb,        CONVERT(decimal(12, 1), MAX(v.total_bytes) / 1073741824.0) AS volume_gb,        CONVERT(decimal(12, 1), MIN(v.available_bytes) / 1073741824.0) AS free_gb,        MAX(ISNULL(v.volume_mount_point, N'(empty)')) AS mount_point,        w.note    FROM sys.master_files AS f    CROSS APPLY (SELECT CHARINDEX(N'/', REVERSE(f.physical_name)) AS s, CHARINDEX(N'\', REVERSE(f.physical_name)) AS b) AS r    CROSS APPLY (SELECT CASE WHEN r.s = 0 THEN r.b WHEN r.b = 0 THEN r.s WHEN r.s < r.b THEN r.s ELSE r.b END AS p) AS pos    CROSS APPLY (SELECT CASE WHEN @linux = 1 AND f.physical_name LIKE N'[A-Za-z]:\%'                             THEN N'Windows path on Linux: C:\ stands for /' ELSE N'' END AS note) AS w    CROSS APPLY sys.dm_os_volume_stats(f.database_id, f.file_id) AS v    GROUP BY LEFT(f.physical_name, LEN(f.physical_name) - pos.p), f.type_desc, w.note    ORDER BY directory, file_type    OPTION (MAXDOP 1);ELSE    SELECT N'Database files per directory' AS directory,           N'not checked: VIEW ANY DEFINITION is missing (sys.master_files would show no rows)' AS note; /* 3 · Windows leftovers in msdb (typical after a migration)   Job steps with a drive letter or UNC path in the command or output file,   job steps of type CmdExec, PowerShell or SSIS, backups of the last 30 days to Windows paths. */IF HAS_PERMS_BY_NAME(N'msdb.dbo.sysjobsteps', N'OBJECT', N'SELECT') = 1   AND HAS_PERMS_BY_NAME(N'msdb.dbo.backupmediafamily', N'OBJECT', N'SELECT') = 1BEGIN    SELECT finding, count, example, N'' AS note    FROM (        SELECT N'Job step with Windows path' AS finding, COUNT(*) AS count,               MIN(j.name + N' / ' + s.step_name) AS example        FROM msdb.dbo.sysjobsteps AS s JOIN msdb.dbo.sysjobs AS j ON j.job_id = s.job_id        WHERE s.command LIKE N'%[A-Za-z]:\%' OR s.command LIKE N'%\\%'           OR s.output_file_name LIKE N'[A-Za-z]:\%' OR s.output_file_name LIKE N'\\%'        UNION ALL        SELECT N'Job step CmdExec, PowerShell or SSIS', COUNT(*),               MIN(j.name + N' / ' + s.step_name + N' (' + s.subsystem + N')')        FROM msdb.dbo.sysjobsteps AS s JOIN msdb.dbo.sysjobs AS j ON j.job_id = s.job_id        WHERE s.subsystem IN (N'CmdExec', N'PowerShell', N'SSIS')        UNION ALL        SELECT N'Backup of the last 30 days to a Windows path', COUNT(*), MIN(m.physical_device_name)        FROM msdb.dbo.backupmediafamily AS m        JOIN msdb.dbo.backupset AS b ON b.media_set_id = m.media_set_id        WHERE b.backup_finish_date >= DATEADD(DAY, -30, GETDATE())          AND (m.physical_device_name LIKE N'[A-Za-z]:\%' OR m.physical_device_name LIKE N'\\%')    ) AS x    OPTION (MAXDOP 1);ENDELSE    SELECT N'Windows leftovers in msdb' AS finding, NULL AS count, NULL AS example,           N'not checked: read access to msdb (sysjobsteps, backupmediafamily) is missing' AS note;

Sample output

From the lab: SQL Server 2022 CU27 in the container, as sa, after we created a database and a backup with a C:\ path for section 1:

Sample output
platform  distribution  release  version      default_data_path     default_backup_path  memory_model
--------  ------------  -------  -----------  --------------------  -------------------  ------------
Linux     Ubuntu        22.04    16.0.4295.3  /var/opt/mssql/data/  /var/opt/mssql/data  CONVENTIONAL

directory              file_type  files  databases  size_gb  volume_gb  free_gb  note
---------------------  ---------  -----  ---------  -------  ---------  -------  ---------------------------------------
/var/opt/mssql/data    LOG        7      7          .1       46.9       21.7
/var/opt/mssql/data    ROWS       12     7          .2       46.9       21.7
C:\var\opt\mssql\data  LOG        1      1          .0       46.9       21.7     Windows path on Linux: C:\ stands for /
C:\var\opt\mssql\data  ROWS       1      1          .0       46.9       21.7     Windows path on Linux: C:\ stands for /

finding                                       count  example
--------------------------------------------  -----  ------------------------------------
Job step with Windows path                    0      NULL
Job step CmdExec, PowerShell or SSIS          0      NULL
Backup of the last 30 days to a Windows path  1      C:\var\opt\mssql\data\l8_cmaster.bak

Columns shortened. Here is how to read it: Data, log and backups all live under /var/opt/mssql/data, on a file system with 21.7 GB free; that is the default and rarely what you want in production (section 1). Two database files and one backup carry Windows paths although they sit in the same Linux directory. After a migration, you will mainly find job steps and backup targets here that still point to drives or shares on Windows. Without VIEW ANY DEFINITION, part 2 reports "not checked", and without read access to msdb, so does part 3.

Method and limits

Measured on Nov 12, 2026 in Linux containers on an Ubuntu VM (cgroup v2, Docker 29.8) with the Microsoft images SQL Server 2017 CU31, 2019 CU32, 2022 CU27 and 2025 CU10 (Ubuntu variants), Developer Edition; a fresh container without network for each version, Instant File Initialization with 4 GB files on the container file system (overlayfs). We ran Ola Hallengren's Maintenance Solution as of Sep 12, 2026 and the First Responder Kit 8.34 unchanged with default settings. What we did not measure ourselves comes from Microsoft Learn (page versions July 2025 to October 2026, read on Nov 12, 2026) and from the source code of the projects named. Limits:

  • Container instead of server: No systemd, no SELinux, no domain, no mounted share. We describe systemctl, SELinux, Active Directory, CIFS and Pacemaker according to the documentation, not our own measurement.
  • Agent on 2017 and 2019: The Agent of these versions signs in over the network to (local),1433. In the container without a network this failed, and it exited. We therefore did not check Ola jobs on these versions through the Agent, only the procedures directly. This is a limit of our setup, not behavior on Linux.
  • No Windows comparison: The "On Windows" column rests on the documentation, not on our own measurements.
  • dbatools: Statements from the source code, not tested.
  • One run per case; times show orders of magnitude.

Further reading

Taking over a Linux instance?

Three things first: run Query 3 and look at where data, log and backups live and whether msdb still contains Windows paths. Check whether the Agent is enabled and whether any maintenance exists, because there cannot be a maintenance plan. And read /var/opt/mssql/mssql.conf and, in the container, the environment variables before you change anything with mssql-conf.

More parts of the series