Linux, containers or Windows? Where SQL Server belongs
The question usually comes from outside. IT is moving to Linux, the development team ships everything as containers, or the next database server is supposed to do without Windows. SQL Server has supported this since 2017. But whether it fits your application is rarely decided by the database engine. It comes down to three questions: Which features does the application use? Who runs the server? And how will it be made highly available?
This page walks through the three questions, summarizes them in a decision table and ends with a check query for your existing instance.
In 60 seconds
- The license is the same. Microsoft licenses SQL Server the same way for Windows and Linux. Costs change at most for the operating system, and for production Microsoft recommends distributions with a support contract.
- For development and CI, the container is usually the better choice. It starts in seconds, has a fixed version and can be thrown away.
- For production, the feature list decides first. Agent jobs with PowerShell or SSIS, FILESTREAM, merge replication or linked servers to Oracle force a rebuild or argue against Linux.
- High availability works, but differently. Pacemaker takes over from the Windows failover cluster. It is a different tool with different operational knowledge.
- Containers in production are supported, under conditions. Microsoft ties support to its images and to the same distribution in container and host. The pitfalls from part 3 come on top.
Three usage scenarios
Development and CI in a container
This is where the benefit is greatest and the risk smallest. In our lab, a container with the Developer edition started in 7 to 14 seconds, every version and every update is its own image tag, and after the test run the container is gone again. The Developer edition is free, but licensed only for development and test. What to watch for so that tests run reproducibly is in part 3: a fixed CU tag instead of latest, waiting for a successful query instead of for the running container, and for tests against Standard on SQL Server 2025, setting MSSQL_PID=StandardDeveloper.
Production on a Linux server or Linux VM
The classic setup, just on a different platform. SQL Server is installed and updated as a package of the distribution, configured with mssql-conf and controlled with systemctl. There is exactly one instance per machine. This path fits if your operations team already patches, monitors and backs up Linux servers and the application needs none of the missing features.
Production in a container or in Kubernetes
Microsoft supports SQL Server in containers on Docker, Kubernetes and OpenShift, but according to the support policy under conditions:
- Official images. Supported are Microsoft's images and your own images that build on them and change nothing under
/opt/mssql; fully self-built images only on a supported distribution. In a support case, Microsoft can require you to reproduce the problem on the official image. - Same distribution in container and host. Distribution and major version of the image must match those of the host. An image based on Ubuntu 22.04 on a host with Ubuntu 24.04 is not supported under this rule.
- Problems of the container runtime you clarify with its vendor, not with Microsoft.
- Features that depend on the Azure Arc agent, such as sign-in through Microsoft Entra, do not exist in the container.
For Kubernetes, Microsoft recommends a StatefulSet with one SQL Server container per pod and, for production, the "Guaranteed" QoS class, that is, equal values for requested and limited resources.
What speaks for Linux
- Operations knows Linux. If patching, monitoring, backup and automation already run on Linux tools, SQL Server fits in there instead of remaining a Windows island.
- The platform is a container cluster anyway. Then SQL Server in a container is the path with the fewest special rules, provided the conditions above are met.
- Many small instances. For test, training and development, containers are simpler than named instances on a Windows server.
- Operating system costs. Windows Server is no longer needed, but for production a support contract for the distribution usually comes in its place. Whether that is cheaper depends on your contracts; the SQL Server license does not change.
What speaks against it
- The application uses features that are missing. The complete list is in the overview. The check query below finds the most common cases on your instance.
- High availability requires new knowledge. Availability groups with automatic failover need Pacemaker on Linux. The listener is an IP address with a DNS entry, and distributed transactions through MSDTC are not supported in availability groups on Linux. A failover cluster instance has no virtual network name; you create the DNS entry yourself.
- Windows logins take setup. The host must join Active Directory, SQL Server needs a keytab with SPNs, and clients connect through the fully qualified host name. Domain local groups are not supported.
- Familiar tools are missing. No Configuration Manager, no maintenance plans from SSMS, no Agent steps for PowerShell. Scripts that access the database server through PowerShell or WMI have to be rebuilt.
- The vendor has a say. For off-the-shelf software, the vendor's approval counts. If it supports SQL Server only on Windows, technical feasibility helps little.
Decision table
| Requirement | Linux server or VM | Container | Note |
|---|---|---|---|
| Application with SQL logins, Agent jobs only in T-SQL | works | works | – |
| Windows logins from Active Directory | with effort | not tested | Keytab, SPNs, connection through the fully qualified host name |
| Agent jobs with PowerShell, CmdExec or SSIS | missing | missing | Convert jobs to T-SQL or schedule them outside SQL Server |
| Alerts | missing | missing | Monitoring outside SQL Server |
| FILESTREAM or FileTables | missing | missing | Redesign file storage |
| Merge replication | missing | missing | – |
| Linked server to Oracle, ODBC or Excel | missing | missing | Microsoft points to PolyBase |
| Reporting or Analysis Services on the same server | missing | missing | Reporting Services on Windows, the catalog may reside on Linux |
| Several instances on one machine | missing | works | one container per instance, own port |
| Automatic failover | with effort | not tested | Pacemaker; a pod restart by Kubernetes is not a failover to a second data copy |
| Distributed transactions (MSDTC) | check | check | not supported in availability groups on Linux |
| Many short-lived test and CI instances | cumbersome | works | Developer edition, fixed CU tag |
"Not tested" means: The sources we read do not answer it unambiguously for containers, and we did not set it up in the lab.
Containers in production: what goes with it
Anyone who runs SQL Server in a container in production needs a few fixed rules in addition to the support conditions. The measurements are in part 3.
- Shut down cleanly. On SQL Server 2022 and 2025,
docker stopdoes not end the instance in an orderly way in today's images. Until Microsoft fixes the startup script, a step is needed that sends the stop signal directly to the SQL Server process, in Kubernetes for example aspreStop. - Set limits yourself. Memory through
MSSQL_MEMORY_LIMIT_MBwith headroom below the container limit, CPUs through pinned CPUs or MAXDOP. SQL Server does not react to a CPU quota alone. - Data on persistent volumes with XFS or ext4, not in the container's file system.
- Updates as a new image with a fixed CU tag, rolled out by a new container on the same volumes.
Check query: What does my instance use that is missing on Linux?
The query runs on your existing instance, usually under Windows, and checks 20 items from Microsoft's list of features missing on Linux: instance settings, logins and endpoints, Agent jobs, alerts and maintenance plans in msdb and, per database, FILESTREAM, FileTables, CLR assemblies and secure enclaves. The result has one row per check, hits first. The classification column says what a hit means:
- missing on Linux: must be rebuilt before a move.
- with effort: works on Linux, but needs setup, for example certificates instead of Windows authentication for endpoints.
- check: depends on what the setting is used for.
Query: What is missing on Linux?
/* PSG Stack · SQL Server on Linux and Docker · Query 1 What does this instance use that is missing on Linux or needs rework? Meant for your existing instance, before you decide on a move to Linux or into a container. Result: one row per check, hits first. !!! WARNING — please read before running !!! - Read-only. The query changes no data and no setting; it only creates three temporary tables in tempdb. - Permissions: simplest as sysadmin. Otherwise VIEW SERVER STATE, VIEW ANY DEFINITION, read access in msdb (db_datareader) and access to the databases. Without VIEW ANY DEFINITION, SQL Server hides other logins, endpoints and linked servers; the note column reports this, as well as anything that could not be read. - MAXDOP 1: every part of the query runs serially. - It goes through every database that is online and that you can access. With a very large number of databases, run it outside peak hours. - Basis: Microsoft's list of features not available on Linux for SQL Server 2022 and 2025 (read on Nov 11 and 12, 2026). No substitute for testing the application; it does not check MSDTC, managed backup, Analysis Services or Reporting Services. - SQL Server 2017 and later. Tested in the PSG lab on 2017, 2019, 2022 and 2025 in Linux containers, with test objects as far as Linux allows creating them (Nov 12, 2026). Not tested on Windows, where it will run before a move; it only reads catalog views that exist there as well.*/SET NOCOUNT ON;SET QUOTED_IDENTIFIER ON; -- sqlcmd starts without it; the XML methods below need it IF OBJECT_ID(N'tempdb..#checks') IS NOT NULL DROP TABLE #checks;IF OBJECT_ID(N'tempdb..#hits') IS NOT NULL DROP TABLE #hits;IF OBJECT_ID(N'tempdb..#db') IS NOT NULL DROP TABLE #db;CREATE TABLE #checks (nr int PRIMARY KEY, area nvarchar(30), check_name nvarchar(120), classification nvarchar(30), note nvarchar(200) NULL);CREATE TABLE #hits (nr int, object_name nvarchar(300));CREATE TABLE #db (name sysname, checked bit); INSERT #checks (nr, area, check_name, classification) VALUES ( 1, N'Instance', N'Named instance (Linux: one default instance per host)', N'with effort'), ( 2, N'Instance', N'xp_cmdshell enabled', N'missing on Linux'), ( 3, N'Instance', N'Custom extended stored procedures', N'missing on Linux'), ( 4, N'Instance', N'Buffer pool extension active', N'missing on Linux'), ( 5, N'Instance', N'Linked servers to sources other than SQL Server', N'missing on Linux'), ( 6, N'Instance', N'Ad Hoc Distributed Queries enabled', N'check'), ( 7, N'Security', N'Windows logins and groups', N'with effort'), ( 8, N'Security', N'Endpoints with Windows authentication', N'with effort'), ( 9, N'Security', N'EKM providers (Linux: Azure Key Vault only, 2022 CU12 and later)', N'check'), (10, N'HA', N'Database mirroring', N'missing on Linux'), (11, N'Agent', N'Job steps CmdExec, PowerShell, SSIS, SSAS, Queue Reader', N'missing on Linux'), (12, N'Agent', N'Alerts', N'missing on Linux'), (13, N'Agent', N'Maintenance plans', N'missing on Linux'), (14, N'Services', N'SSIS catalog (SSISDB) present', N'check'), (15, N'Databases', N'Merge replication (published)', N'missing on Linux'), (16, N'Databases', N'Stretch Database', N'missing on Linux'), (17, N'Databases', N'FILESTREAM filegroups', N'missing on Linux'), (18, N'Databases', N'FileTables', N'missing on Linux'), (19, N'Databases', N'CLR assemblies with EXTERNAL_ACCESS or UNSAFE', N'missing on Linux'), (20, N'Databases', N'Always Encrypted with secure enclaves', N'missing on Linux'); /* Without VIEW ANY DEFINITION the catalog views show only part of the objects */IF HAS_PERMS_BY_NAME(NULL, NULL, 'VIEW ANY DEFINITION') = 0 UPDATE #checks SET note = N'incomplete: only partly visible without VIEW ANY DEFINITION' WHERE nr IN (3, 5, 7, 8, 9, 19); /* 1 · Instance and security */INSERT #hits SELECT 1, CONVERT(nvarchar(300), SERVERPROPERTY('InstanceName')) WHERE SERVERPROPERTY('InstanceName') IS NOT NULL;INSERT #hits SELECT 2, N'xp_cmdshell = 1' FROM sys.configurations WHERE name = N'xp_cmdshell' AND CONVERT(int, value_in_use) = 1 OPTION (MAXDOP 1);INSERT #hits SELECT 3, name FROM master.sys.extended_procedures WHERE is_ms_shipped = 0 OPTION (MAXDOP 1);BEGIN TRY INSERT #hits SELECT 4, path FROM sys.dm_os_buffer_pool_extension_configuration WHERE state <> 0 OPTION (MAXDOP 1);END TRY BEGIN CATCH UPDATE #checks SET note = N'not readable: ' + ERROR_MESSAGE() WHERE nr = 4; END CATCH;INSERT #hits SELECT 5, name + N' (' + provider + N')' FROM sys.servers WHERE is_linked = 1 AND NOT (product = N'SQL Server' OR provider LIKE N'SQLNCLI%' OR provider LIKE N'MSOLEDBSQL%' OR provider = N'SQLOLEDB') OPTION (MAXDOP 1);INSERT #hits SELECT 6, N'Ad Hoc Distributed Queries = 1' FROM sys.configurations WHERE name = N'Ad Hoc Distributed Queries' AND CONVERT(int, value_in_use) = 1 OPTION (MAXDOP 1);INSERT #hits SELECT 7, name FROM sys.server_principals WHERE type IN ('U', 'G') AND name NOT LIKE N'NT SERVICE\%' AND name NOT LIKE N'NT AUTHORITY\%' AND name NOT LIKE N'BUILTIN\%' OPTION (MAXDOP 1);INSERT #hits SELECT 8, name + N' (' + connection_auth_desc + N')' FROM sys.database_mirroring_endpoints WHERE connection_auth_desc LIKE N'%NTLM%' OR connection_auth_desc LIKE N'%KERBEROS%' OR connection_auth_desc LIKE N'%NEGOTIATE%' OPTION (MAXDOP 1);INSERT #hits SELECT 9, name FROM sys.cryptographic_providers OPTION (MAXDOP 1);INSERT #hits SELECT 10, DB_NAME(database_id) FROM sys.database_mirroring WHERE mirroring_guid IS NOT NULL OPTION (MAXDOP 1);INSERT #hits SELECT 14, name FROM sys.databases WHERE name = N'SSISDB' OPTION (MAXDOP 1);INSERT #hits SELECT 15, name FROM sys.databases WHERE is_merge_published = 1 OPTION (MAXDOP 1);IF EXISTS (SELECT 1 FROM sys.all_columns WHERE object_id = OBJECT_ID(N'sys.databases') AND name = N'is_remote_data_archive_enabled') EXEC (N'INSERT #hits SELECT 16, name FROM sys.databases WHERE is_remote_data_archive_enabled = 1 OPTION (MAXDOP 1);'); /* 2 · SQL Server Agent and maintenance plans in msdb */BEGIN TRY INSERT #hits SELECT 11, 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', N'ANALYSISQUERY', N'ANALYSISCOMMAND', N'QueueReader') OPTION (MAXDOP 1);END TRY BEGIN CATCH UPDATE #checks SET note = N'not readable: ' + ERROR_MESSAGE() WHERE nr = 11; END CATCH;BEGIN TRY INSERT #hits SELECT 12, name FROM msdb.dbo.sysalerts OPTION (MAXDOP 1);END TRY BEGIN CATCH UPDATE #checks SET note = N'not readable: ' + ERROR_MESSAGE() WHERE nr = 12; END CATCH;BEGIN TRY INSERT #hits SELECT 13, name FROM msdb.dbo.sysmaintplan_plans OPTION (MAXDOP 1);END TRY BEGIN CATCH UPDATE #checks SET note = N'not readable: ' + ERROR_MESSAGE() WHERE nr = 13; END CATCH; /* 3 · Per database: FILESTREAM, FileTables, CLR, secure enclaves */DECLARE @db sysname, @sql nvarchar(max), @enclave bit = CASE WHEN EXISTS (SELECT 1 FROM sys.all_columns WHERE object_id = OBJECT_ID(N'sys.column_master_keys') AND name = N'allow_enclave_computations') THEN 1 ELSE 0 END;INSERT #db SELECT name, 0 FROM sys.databases WHERE database_id <> 2 OPTION (MAXDOP 1);DECLARE dbs CURSOR LOCAL FAST_FORWARD FOR SELECT name FROM sys.databases WHERE database_id <> 2 AND state = 0 AND HAS_DBACCESS(name) = 1;OPEN dbs; FETCH NEXT FROM dbs INTO @db;WHILE @@FETCH_STATUS = 0BEGIN SET @sql = N'USE ' + QUOTENAME(@db) + N'; INSERT #hits SELECT 17, DB_NAME() + N'': '' + name FROM sys.database_files WHERE type = 2 OPTION (MAXDOP 1); INSERT #hits SELECT 18, DB_NAME() + N'': '' + name FROM sys.tables WHERE is_filetable = 1 OPTION (MAXDOP 1); INSERT #hits SELECT 19, DB_NAME() + N'': '' + name + N'' ('' + permission_set_desc + N'')'' FROM sys.assemblies WHERE is_user_defined = 1 AND permission_set > 1 OPTION (MAXDOP 1);' + CASE WHEN @enclave = 1 THEN N' INSERT #hits SELECT 20, DB_NAME() + N'': '' + name FROM sys.column_master_keys WHERE allow_enclave_computations = 1 OPTION (MAXDOP 1);' ELSE N'' END; BEGIN TRY EXEC sys.sp_executesql @sql; UPDATE #db SET checked = 1 WHERE name = @db; END TRY BEGIN CATCH END CATCH; FETCH NEXT FROM dbs INTO @db;END;CLOSE dbs; DEALLOCATE dbs;UPDATE #checks SET note = ISNULL(note + N'; ', N'') + CONVERT(nvarchar(10), (SELECT COUNT(*) FROM #db WHERE checked = 1)) + N' databases checked' + ISNULL(N', not checked (offline or no access): ' + STUFF((SELECT N', ' + name FROM #db WHERE checked = 0 FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 2, N''), N'')WHERE nr BETWEEN 17 AND 20; /* Result: hits first, at most five examples per check */SELECT c.nr, c.area, c.check_name, COUNT(h.object_name) AS hits, CASE WHEN COUNT(h.object_name) = 0 THEN N'-' ELSE c.classification END AS classification, ISNULL(STUFF((SELECT TOP (5) N', ' + h2.object_name FROM #hits AS h2 WHERE h2.nr = c.nr ORDER BY h2.object_name FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 2, N''), N'') AS examples, ISNULL(c.note, N'') AS noteFROM #checks AS c LEFT JOIN #hits AS h ON h.nr = c.nrGROUP BY c.nr, c.area, c.check_name, c.classification, c.noteORDER BY CASE WHEN COUNT(h.object_name) > 0 THEN 0 ELSE 1 END, c.nrOPTION (MAXDOP 1);Sample output
From the lab: SQL Server 2025 CU10 in a container, with test objects, run with the recommended minimum permissions. The note column is omitted; of the rows without hits, only the first three are shown here:
nr area check_name hits classification examples
-- --------- ------------------------------------------------------- ---- ---------------- ------------------------------------------------
6 Instance Ad Hoc Distributed Queries enabled 1 check Ad Hoc Distributed Queries = 1
8 Security Endpoints with Windows authentication 1 with effort lx_hadr (NEGOTIATE)
11 Agent Job steps CmdExec, PowerShell, SSIS, SSAS, Queue Reader 2 missing on Linux lx_job / cmd (CmdExec), lx_job / ps (PowerShell)
12 Agent Alerts 1 missing on Linux lx_alert
20 Databases Always Encrypted with secure enclaves 1 missing on Linux lx_app: lx_cmk
1 Instance Named instance (Linux: one default instance per host) 0 -
2 Instance xp_cmdshell enabled 0 -
3 Instance Custom extended stored procedures 0 -
…How to read it: The two job steps and the alert must be replaced before a move, the endpoint needs certificates, and the column master key for secure enclaves shows that a database uses Always Encrypted with enclaves, which is missing on Linux.
What stood out in the lab was what SQL Server on Linux allows you to create: All four versions accepted the CmdExec and the PowerShell job step, the alert and the endpoint with Windows authentication. In contrast, they immediately rejected xp_cmdshell, FILESTREAM and a linked server over ODBC. So an object that can be created does not necessarily work, and a move by script can silently carry such objects along.
A result without hits does not mean the application runs on Linux. The query does not see what the application itself does, such as Windows paths in code or Windows logins of the clients. It does not check MSDTC, managed backup, or Analysis and Reporting Services. If the note column says "incomplete" or "not readable", the executing login lacks permissions.
Method and limits
The statements on license, platforms, features, support policy, Kubernetes and high availability come from Microsoft Learn (read on Nov 11 and 12, 2026). We read the feature list for SQL Server 2022 and 2025, not separately for 2017 and 2019. We tested the check query on Nov 12, 2026 on SQL Server 2017 CU31, 2019 CU32, 2022 CU27 and 2025 CU10 in Linux containers: on an empty instance, with test objects and with three permission levels. Limits:
- We did not run it under Windows, where the query will usually run before a move. It only reads catalog views that exist there as well.
- Several checks could not be triggered with real objects on Linux, because FILESTREAM, merge replication, mirroring, named instances or Windows logins cannot be created there. These branches ran only without hits.
- We did not set up Pacemaker, Active Directory or Kubernetes in the lab. We did not measure whether objects that can be created but are not supported fail at runtime.
Further reading
- Microsoft Learn: "Editions and supported features of SQL Server 2025 on Linux", "SQL Server on Linux frequently asked questions", "Support policy for SQL Server" (section on containers), "Deploy SQL Server Linux containers on Kubernetes with StatefulSets".
- Microsoft Learn: "Availability groups for SQL Server on Linux" and the tutorial on Active Directory authentication for SQL Server on Linux.
- In this series: Overview, Part 3: What goes wrong in the container.
- At PSG: FCI vs. availability groups.
Before you decide
A move to Linux is rarely a pure infrastructure project. Run query 1 on today's instance, clarify the approval with the application vendor, and decide on high availability and sign-in before you order servers. We are glad to do this check together with you before planning and migration begin.