Monthly Archives: September 2019

Memory Settings for Running SQL Server in Kubernetes

People often ask me what’s the number one thing to look out for when running SQL Server on Kubernetes…the answer is memory settings. In this post, we’re going to dig into why you need to configure resource limits in your SQL Server’s Pod Spec when running SQL Server workloads in Kubernetes. I’m running these demos in Azure Kubernetes Service (AKS), but these concepts apply to any SQL Server environment running in Kubernetes. 

Let’s deploy SQL Server in a Pod without any resource limits.  In the yaml below, we’re using a Deployment to run one SQL Server Pod with a PersistentVolumeClaim for our instance directory and also frontending the Pod with a Service for access. 

apiVersion: apps/v1
kind: Deployment
  name: mssql-deployment-2017
  replicas: 1
    type: Recreate
        app: mssql-2017
        app: mssql-2017
      hostname: sql3
      - name: mssql
        image: ''
        - containerPort: 1433
        - name: ACCEPT_EULA
          value: "Y"
        - name: SA_PASSWORD
              name: mssql
              key: SA_PASSWORD
        - name: mssqldb
          mountPath: /var/opt/mssql
      - name: mssqldb
          claimName: pvc-sql-2017
apiVersion: v1
kind: PersistentVolumeClaim
  name: pvc-sql-2017
  - ReadWriteOnce
      storage: 50Gi
  storageClassName: managed-premium
apiVersion: v1
kind: Service
  name: mssql-svc-2017
    app: mssql-2017
    - protocol: TCP
      port: 1433
      targetPort: 1433
  type: LoadBalancer

Running a Workload Against our Pod…then BOOM!

With that Pod deployed, I loaded up a HammerDB TPC-C test with about 10GB of data and drove a workload against our SQL Server. Then while monitoring the workload…boom HammerDB throws connection errors and crashes. Let’s look at why.

First thing’s first, let’s check the Pods status with kubectl get pods. We’ll that’s interesting I have 13 Pods. 1 has a Status of Running and the remainder have are Evicted

kubectl get pods
NAME                                     READY   STATUS    RESTARTS   AGE
mssql-deployment-2017-8698fb8bf5-2pw2z   0/1     Evicted   0          8m24s
mssql-deployment-2017-8698fb8bf5-4bn6c   0/1     Evicted   0          8m23s
mssql-deployment-2017-8698fb8bf5-4pw7d   0/1     Evicted   0          8m25s
mssql-deployment-2017-8698fb8bf5-54k6k   0/1     Evicted   0          8m27s
mssql-deployment-2017-8698fb8bf5-96lzf   0/1     Evicted   0          8m26s
mssql-deployment-2017-8698fb8bf5-clrbx   0/1     Evicted   0          8m27s
mssql-deployment-2017-8698fb8bf5-cp6ml   0/1     Evicted   0          8m27s
mssql-deployment-2017-8698fb8bf5-ln8zt   0/1     Evicted   0          8m27s
mssql-deployment-2017-8698fb8bf5-nmq65   0/1     Evicted   0          8m21s
mssql-deployment-2017-8698fb8bf5-p2mvm   0/1     Evicted   0          25h
mssql-deployment-2017-8698fb8bf5-stzfw   0/1     Evicted   0          8m23s
mssql-deployment-2017-8698fb8bf5-td24w   1/1     Running   0          8m20s
mssql-deployment-2017-8698fb8bf5-wpgcx   0/1     Evicted   0          8m22s

What Just Happened?

Let’s keep digging and look at kubectl get events to see if that can help us sort out what’s happening…reading through these events a lot is going on. Let’s start at the top, we can see that our original Pod mssql-deployment-2017-8698fb8bf5-p2mvm is Killed and the line below that tells us why. The Node had a MemoryPressure condition. A few lines below that we see that our mssql container was using 4461532Ki which exceeded its request of 0 (more on why it’s 0 in a bit). So then our Deployment Controller sees that our Pod is no longer up and running so the Deployment controller does what it’s supposed to do start a new Pod in the place of the failed Pod.
The scheduler in Kubernetes will try to place a Pod back onto the same Node if the Node is still available, in our case aks-agentpool-43452558-0. And each time the scheduler places the Pod back onto the same Node it find that the MemoryPressure condition is still true, so after the 10th try the scheduler selects a new Node, aks-agentpool-43452558-3 to run our Pod. And in the last line of the output below we can see that once the workload is moved to aks-agentpool-43452558-3 the MemoryPressure condition goes away on aks-agentpool-43452558-0 since it’s no longer running our workload. 
kubectl get events --sort-by=.metadata.creationTimestamp
LAST SEEN   TYPE      REASON                      OBJECT                                        MESSAGE
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-clrbx    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-clrbx to aks-agentpool-43452558-0
17m         Warning   EvictionThresholdMet        node/aks-agentpool-43452558-0                 Attempting to reclaim memory
17m         Normal    SuccessfulCreate            replicaset/mssql-deployment-2017-8698fb8bf5   Created pod: mssql-deployment-2017-8698fb8bf5-clrbx
17m         Normal    SuccessfulCreate            replicaset/mssql-deployment-2017-8698fb8bf5   Created pod: mssql-deployment-2017-8698fb8bf5-ln8zt
17m         Normal    Killing                     pod/mssql-deployment-2017-8698fb8bf5-p2mvm    Stopping container mssql
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-54k6k    The node had condition: [MemoryPressure].
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-p2mvm    The node was low on resource: memory. Container mssql was using 4461532Ki, which exceeds its request of 0.
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-cp6ml    The node had condition: [MemoryPressure].
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-cp6ml    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-cp6ml to aks-agentpool-43452558-0
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-54k6k    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-54k6k to aks-agentpool-43452558-0
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-clrbx    The node had condition: [MemoryPressure].
17m         Normal    SuccessfulCreate            replicaset/mssql-deployment-2017-8698fb8bf5   Created pod: mssql-deployment-2017-8698fb8bf5-cp6ml
17m         Normal    SuccessfulCreate            replicaset/mssql-deployment-2017-8698fb8bf5   Created pod: mssql-deployment-2017-8698fb8bf5-54k6k
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-ln8zt    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-ln8zt to aks-agentpool-43452558-0
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-96lzf    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-96lzf to aks-agentpool-43452558-0
17m         Normal    SuccessfulCreate            replicaset/mssql-deployment-2017-8698fb8bf5   Created pod: mssql-deployment-2017-8698fb8bf5-96lzf
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-ln8zt    The node had condition: [MemoryPressure].
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-96lzf    The node had condition: [MemoryPressure].
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-4pw7d    The node had condition: [MemoryPressure].
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-4pw7d    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-4pw7d to aks-agentpool-43452558-0
17m         Normal    SuccessfulCreate            replicaset/mssql-deployment-2017-8698fb8bf5   Created pod: mssql-deployment-2017-8698fb8bf5-4pw7d
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-2pw2z    The node had condition: [MemoryPressure].
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-2pw2z    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-2pw2z to aks-agentpool-43452558-0
17m         Normal    SuccessfulCreate            replicaset/mssql-deployment-2017-8698fb8bf5   Created pod: mssql-deployment-2017-8698fb8bf5-2pw2z
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-4bn6c    The node had condition: [MemoryPressure].
17m         Normal    SuccessfulCreate            replicaset/mssql-deployment-2017-8698fb8bf5   Created pod: mssql-deployment-2017-8698fb8bf5-4bn6c
17m         Normal    SuccessfulCreate            replicaset/mssql-deployment-2017-8698fb8bf5   Created pod: mssql-deployment-2017-8698fb8bf5-stzfw
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-4bn6c    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-4bn6c to aks-agentpool-43452558-0
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-stzfw    The node had condition: [MemoryPressure].
17m         Normal    SuccessfulCreate            replicaset/mssql-deployment-2017-8698fb8bf5   (combined from similar events): Created pod: mssql-deployment-2017-8698fb8bf5-td24w
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-wpgcx    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-wpgcx to aks-agentpool-43452558-0
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-wpgcx    The node had condition: [MemoryPressure].
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-stzfw    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-stzfw to aks-agentpool-43452558-3
17m         Warning   Evicted                     pod/mssql-deployment-2017-8698fb8bf5-nmq65    The node had condition: [MemoryPressure].
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-nmq65    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-nmq65 to aks-agentpool-43452558-0
17m         Normal    NodeHasInsufficientMemory   node/aks-agentpool-43452558-0                 Node aks-agentpool-43452558-0 status is now: NodeHasInsufficientMemory
17m         Normal    Scheduled                   pod/mssql-deployment-2017-8698fb8bf5-td24w    Successfully assigned default/mssql-deployment-2017-8698fb8bf5-td24w to aks-agentpool-43452558-3
16m         Normal    SuccessfulAttachVolume      pod/mssql-deployment-2017-8698fb8bf5-td24w    AttachVolume.Attach succeeded for volume "pvc-f35b270a-e063-11e9-9b6d-ee8baa4f9319"
15m         Normal    Pulling                     pod/mssql-deployment-2017-8698fb8bf5-td24w    Pulling image ""
15m         Normal    Pulled                      pod/mssql-deployment-2017-8698fb8bf5-td24w    Successfully pulled image ""
15m         Normal    Started                     pod/mssql-deployment-2017-8698fb8bf5-td24w    Started container mssql
15m         Normal    Created                     pod/mssql-deployment-2017-8698fb8bf5-td24w    Created container mssql
12m         Normal    NodeHasSufficientMemory     node/aks-agentpool-43452558-0                 Node aks-agentpool-43452558-0 status is now: NodeHasSufficientMemory
But guess what…we’re going to have the same problem on this new Node. If we run our workload again, our memory allocation will grow and Kubernetes will kill the Pod again once the MemoryPressure condition is met. So what do we do…how can we prevent our nodes from going into a MemoryPressure condition? 

Understanding Allocatable Memory in Kubernetes 

Using kubectl describe nodein the output below there’s a section Allocatable. In there we can see that amount of allocatable resources on this Node in terms of CPU, disk, RAM and Pods. These are the amount of resources available to run user Pods on this Node. And there we see the amount of allocatable memory is 4667840Ki (~4.45GB) so we have about that much memory to run our workloads. The amount here is a function of the amount of memory in the Node and reservations made by Kubernetes for system functions, more on that here. Our AKS cluster VMs are Standard DS2 v2 which have 2vCPU and 7GB of RAM, so about 2.55GB is reserved for other uses.  The output below is from after our Pod was evicted so we can see the LastTransitionTime shows the last time a condition occurred and for MemoryPressure we can see an event at 7:53 AM. The other LastTransitionTimes are from when the Node was started. Another key point is in the Events section where we can see the conditions change state.
kubectl describe nodes aks-agentpool-43452558-0
Name:               aks-agentpool-43452558-0
...output omitted...
Unschedulable:      false
  Type                 Status  LastHeartbeatTime                 LastTransitionTime                Reason                       Message
  ----                 ------  -----------------                 ------------------                ------                       -------
  NetworkUnavailable   False   Tue, 10 Sep 2019 16:20:00 -0500   Tue, 10 Sep 2019 16:20:00 -0500   RouteCreated                 RouteController created a route
  MemoryPressure       False.  Sat, 28 Sep 2019 07:58:56 -0500.  Sat, 28 Sep 2019 07:53:55 -0500.  KubeletHasSufficientMemory   kubelet has sufficient memory available
  DiskPressure         False   Sat, 28 Sep 2019 07:58:56 -0500   Tue, 10 Sep 2019 16:18:27 -0500   KubeletHasNoDiskPressure     kubelet has no disk pressure
  PIDPressure          False   Sat, 28 Sep 2019 07:58:56 -0500   Tue, 10 Sep 2019 16:18:27 -0500   KubeletHasSufficientPID      kubelet has sufficient PID available
  Ready                True    Sat, 28 Sep 2019 07:58:56 -0500   Tue, 10 Sep 2019 16:18:27 -0500   KubeletReady                 kubelet is posting ready status. AppArmor enabled
  Hostname:    aks-agentpool-43452558-0
 attachable-volumes-azure-disk:  8
 cpu:                            2
 ephemeral-storage:              101584140Ki
 hugepages-1Gi:                  0
 hugepages-2Mi:                  0
 memory:                         7113152Ki
 pods:                           110
  attachable-volumes-azure-disk:  8
 cpu:                            1931m
 ephemeral-storage:              93619943269
 hugepages-1Gi:                  0
 hugepages-2Mi:                  0
 memory: 4667840Ki  pods:                           110
...output omitted...
Type     Reason                     Age                  From                               Message
  ----     ------                     ----                 ----                               -------
  Warning  EvictionThresholdMet       10m                  kubelet, aks-agentpool-43452558-0  Attempting to reclaim memory
  Normal   NodeHasInsufficientMemory  10m                  kubelet, aks-agentpool-43452558-0  Node aks-agentpool-43452558-0 status is now: NodeHasInsufficientMemory
  Normal   NodeHasSufficientMemory    5m15s (x2 over 14d)  kubelet, aks-agentpool-43452558-0  Node aks-agentpool-43452558-0 status is now: NodeHasSufficientMemory

SQL Server’s View of Memory on Kubernetes Nodes

When using a Pod with no memory limits defined in the Pod Spec (which is why we saw 0 for the limits in the Event entry) SQL Server sees 5557MB (~5.4GB) memory available and thinks it has that to use. Why is that? Well, SQL Server on Linux looks at the base OS to see how much memory is available on the system and by default uses approximately 80% of that memory due its architecture (SQLPAL).
2019-09-28 14:46:16.23 Server      Detected 5557 MB of RAM. This is an informational message; no user action is required. 
This is bad news in our situation, Kubernetes has only 4667840Ki (~4.45GB) to allocate before setting the MemoryPressure condition which will cause our Pod to be Evicted and Terminated. And as with our workload running SQL Server allocates memory, primarily to the buffer pool, and it exceeds the Allocatable amount of memory for the Node Kubernetes kills our Pod to protect the Node and the cluster as a whole. 

Configuring Pod Limits for SQL Server

So how do we fix all of this? We need to set a resource limit in our Pod Spec. Limits allow us to control the amount of a particular resource exposed to a Pod. And in our case, we want to limit the amount of memory we want SQL Server to see. In our environment we know we have  4667840Ki (~4.45GB) of Allocatable memory for user Pods on Nodes so lets set a value lower than that…and to be super safe I’m going to use 3GB. In the code below you can see in the Pod Spec for our mssql container we have a section for resources, limits and a value of memory: “3Gi”.

      hostname: sql3
      - name: mssql
        image: ''
        - containerPort: 1433
        - name: ACCEPT_EULA
          value: "Y"
        - name: SA_PASSWORD
              name: mssql
              key: SA_PASSWORD
            memory: "3Gi"
        - name: mssqldb
          mountPath: /var/opt/mssql
      - name: mssqldb
          claimName: pvc-sql-system-2017
With this configured we limit the amount of memory SQL Server sees to 3GB. Given that the container is running SQL Server on Linux, SQL Server will actually see about 80% of that 2458MB
2019-09-28 14:01:46.16 Server      Detected 2458 MB of RAM. This is an informational message; no user action is required.


With that, I hope you can see why I consider memory settings the number one thing to look out for when deploying SQL Server in Kubernetes.  Setting appropriate values will ensure that your SQL Server instance on Kubernetes stays up and running and happily with the other workloads you have running in your cluster.  What’s the best value to set? We need to take into account the amount of memory on the Node, the amount of memory we need to run our workload in SQL Server, and the reservations needed by both Kubernetes and SQLPAL. Additionally, we should set max server memory instance level setting inside of SQL Server to limit the amount of memory that’s allocatable. My suggestion to you is to configure both a resource limit at the Pod Spec and configure max server memory at the instance level.

If you want to read more about resource management and Pod eviction check out this resources:





Restoring Backups from Azure Blob with dbatools

Recently I needed to write a PowerShell script that could build a backup set from a collection of backups stored in Azure Blob Storage without any backup history available from MSDB. And as with all things SQL Server and PowerShell related I went straight to to see if Restore-DbaDatabase was up to the task…and of course, it is…let’s talk about how I solved this challenge.

When restoring from Azure Blob, the main challenge you have is accessing the blobs and building a backup set. For this process, you’ll need access to the Storage Account via PowerShell and you’ll have to have define a Credential on your SQL Instance that has access to the Storage Account.

Building a BackupSet

The first step is to get a collection of backups we want to build our backup set from. In my scenario, I know that we have weekly fulls, so I can limit the search space by getting only backups within the last seven days from the total set of backups in the Storage Account. In this code section, we use Get-AzStorageBlob to get the collection of Blobs (backups) with the parameters Context, which is used to define our the location of and log in to the Storage Account where the backups are stored, and Container which is the container the blobs are stored in.

$DaysBack = 7
$DateBack = (Get-Date).ToUniversalTime().AddDays(-$DaysBack)
$BlobsToRestoreFrom = Get-AzStorageBlob -Container $containerName -Context $storageContext  |  Where-Object { $_.LastModified -gt $DateBack }

 Once we have a collection of Blobs to work with we need to build a second set the URI and Blob name in a format that Get-DbaBackupInformation likes

#Build a list of backup files available in URI/Name format
$FilesToRestore = @()
foreach ( $Blob in $BlobsToRestoreFrom ){
    $FilesToRestore += "$($cbc.Uri.AbsoluteUri)/$($Blob.Name)”

With that set of properly formatted file references, let’s hand that information into Get-DbaBackupInformation to build a set of backup history from the files. In this code I’m referencing a server I want to restore to, SqlInstance, a SqlCredential used to log into that server, filtering the databases from the backup set to the databases I want to restore with DatabaseName and defining the Path to the backups. That’s the set we just build and stored in $FilesToRestore in the code above. Store the output of Get-DbaBackupInformation in a variable for use later.

$BackupHistory = Get-DbaBackupInformation `
    -SqlInstance $RestoreServer -SqlCredential $RestoreCredential `
    -DatabaseName @(‘DB1’,'DB2') `
    -Path $FilesToRestore

Restoring using Restore-DbaDatabase

 Once we have our restore history we can pipe that into Restore-DbaDatabase and off our Instance goes restoring the databases. 

$BackupHistory | Restore-DbaDatabase -SqlInstance $RestoreServer -SqlCredential $RestoreCredential

Huge shout out to Shawn Melton and Stuart Moore for their help on this and of course to the whole team for everything they do!

Using kubectl logs to read the SQL Server Error Log in Kubernetes

When working with SQL Server running containers the Error Log is written to standard out. Kubernetes will expose that information to you via kubectl. Let’s check out how it works.

If we start up a Pod running SQL Server and grab the Pod name

kubectl get pods
NAME                                READY   STATUS    RESTARTS   AGE
mssql-deployment-56d8dbb7b7-hrqwj   1/1     Running   0          22m

We can use follow flag and that will continuously write the error log to your console, similar to using tail with the -f option. If you remove the follow flag it will write the current log to your console. This can be useful in debugging failed startups or in the case below, monitoring the status of a database restore. When finished you can use CTRL+C to break out and return back to your prompt.

kubectl logs mssql-deployment-56d8dbb7b7-hrqwj --follow

Will yield the following output

SQL Server 2019 will run as non-root by default.
This container is running as user root.
To learn more visit
This is an evaluation version.  There are [157] days left in the evaluation period.
2019-09-12 18:11:06.74 Server      Setup step is copying system data file 'C:\templatedata\master.mdf' to '/var/opt/mssql/data/master.mdf'.
2019-09-12 18:11:06.82 Server      Did not find an existing master data file /var/opt/mssql/data/master.mdf, copying the missing default master and other system database files. If you have moved the database location, but not moved the database files, startup may fail. To repair: shutdown SQL Server, move the master database to configured location, and restart.
2019-09-12 18:11:06.83 Server      Setup step is copying system data file 'C:\templatedata\mastlog.ldf' to '/var/opt/mssql/data/mastlog.ldf'.
2019-09-12 18:11:06.85 Server      Setup step is copying system data file 'C:\templatedata\model.mdf' to '/var/opt/mssql/data/model.mdf'.
2019-09-12 18:11:06.87 Server      Setup step is copying system data file 'C:\templatedata\modellog.ldf' to '/var/opt/mssql/data/modellog.ldf'.
2019-09-12 18:11:06.89 Server      Setup step is copying system data file 'C:\templatedata\msdbdata.mdf' to '/var/opt/mssql/data/msdbdata.mdf'.
...output omitted...
2019-09-12 18:11:12.37 spid9s      Database 'msdb' running the upgrade step from version 903 to version 904.
2019-09-12 18:11:12.52 spid9s      Recovery is complete. This is an informational message only. No user action is required.
2019-09-12 18:11:12.55 spid20s     The default language (LCID 0) has been set for engine and full-text services.
2019-09-12 18:11:12.87 spid20s     The tempdb database has 2 data file(s).
2019-09-12 18:14:29.78 spid56      Attempting to load library 'xpstar.dll' into memory. This is an informational message only. No user action is required.
2019-09-12 18:14:29.84 spid56      Using 'xpstar.dll' version '2019.150.1900' to execute extended stored procedure 'xp_instance_regread'. This is an informational message only; no user action is required.
2019-09-12 18:14:30.00 spid56      Attempting to load library 'xplog70.dll' into memory. This is an informational message only. No user action is required.
2019-09-12 18:14:30.05 spid56      Using 'xplog70.dll' version '2019.150.1900' to execute extended stored procedure 'xp_msver'. This is an informational message only; no user action is required.
...output omitted...
2019-09-12 18:32:32.40 spid66      [5]. Feature Status: PVS: 0. CTR: 0. ConcurrentPFSUpdate: 1.
2019-09-12 18:32:32.41 spid66      Starting up database ‘DB1'.
2019-09-12 18:32:32.72 spid66      The database 'DB1' is marked RESTORING and is in a state that does not allow recovery to be run.
2019-09-12 18:32:37.44 Backup      Database was restored: Database: DB1  creation date(time): 2019/05/11(13:32:05), first LSN: 148853:1000384:1, last LSN: 148853:1067344:1, number of dump devices: 1, device information: (FILE=1, TYPE=URL: {''}). Informational message. No user action required.

New Pluralsight Course – Managing Kubernetes Controllers and Deployments

My new course “Managing Kubernetes Controllers and Deployments” in now available on Pluralsight here! Check out the trailer here or if you want to dive right in go here! This course offers practical tips from my experiences managing Kubernetes Clusters and workloads for Centino Systems clients.

This course targets IT professionals that design and maintain Kubernetes and container based solutions.The course can be used by both the IT pro learning new skills and the system administrator or developer preparing for using Kubernetes both on premises and in the Cloud. 

Let’s take your Kubernetes administration and configuration skills to the next level and get you started now!

The modules of the course are:

  • Using Controllers to Deploy Applications and Deployment Basics – In this module we dive into what Controllers are and how they can be used to deploy applications in Kubernetes. We’ll introduce several core controller types and look at the fundamentals of using the Deployment Controller to deploy applications and take a deep dive into the Controller operations of ReplicaSets.
  • Maintaining Applications with Deployments – In this demo-heavy module, we look closer at Deployments and learn how we can maintain our container based applications. We look at updating Deployments, controlling rollouts and using updateStrategy and readinessProbes to ensure successful rollouts. We’ll also cover what to do when things go wrong and learn how to pause and rollback rollouts.
  • Deploying and Maintaining Applications with DaemonSets and Jobs – In this module, we introduce the DaemonSet controller and how it’s used to deploy applications to all Nodes or a subset of Nodes in our cluster, we’ll also cover DaemonSet operations such as updating and controlling rollouts. We wrap up the course with a look at how we can use Jobs and CronJobs to ensure work completes in our cluster. 


Check out the course at Pluralsight!


Workshop – Kubernetes Zero to Hero at SQL Saturday Denver!

Pre-conference Workshop at SQLSaturday Denver

I’m proud to announce that I will be be presenting an all day pre-conference workshop at SQL Saturday Denver on October 11th 2019! This one won’t let you down! 

The workshop is Kubernetes Zero to Hero – Installation, Configuration, and Application Deployment” 


Here’s the abstract for the workshop

Modern application deployment needs to be fast and consistent to keep up with business objectives and Kubernetes is quickly becoming the standard for deploying container-based applications, fast. In this day-long session, we will start with an architectural overview of a Kubernetes cluster and how it manages application state. Then we will learn how to build a production-ready cluster. With our cluster up and running, we will learn how to interact with our cluster, common administrative tasks, then wrap up with how to deploy applications and SQL Server. At the end of the session, you will know how to set up a Kubernetes cluster, manage a cluster, deploy applications and databases, and how to keep everything up and running.

Session Objectives

  • Introduce Kubernetes Cluster Components
  • Introduce Kubernetes API Objects and Controllers
  • Installing Kubernetes
  • Interacting with your cluster
  • Storing persistent data in Kubernetes
  • Deploying Applications in Kubernetes
  • Deploying SQL Server in Kubernetes
  • High Availability scenarios in Kubernetes


How much does it cost?

The full day training event is $150 per attendee.

What can I bring into the event?
WiFi at the location is limited. The workshop will be primarily demonstration based. Code will be made available for download prior to the event if you would like to follow along during the session.

How can I contact the organizer with any questions?
Please feel free to email me with any questions:

What’s the refund policy?
7 days: Attendees can receive refunds up to 1 days before your event start date.

Do I need to know SQL Server or Kubernetes to attend this workshop?
No, while we will be focusing on deploying SQL Server in Kubernetes, no prior knowledge of SQL Server or Kubernetes is needed. We will build up our Kubernetes skills using SQL Server as the primary application we will deploy.

What are the prerequisites for the workshop?
All examples will be executed at the command line, so proficiency at a command line is required. Platform dependent (Linux/Windows,Cloud CLIs) configurations and commands will be introduced and discussed in the workshop.  

Using strace inside a SQL Server Container

So, if you’ve been following my blog you know my love for internals. Well, I needed to find out exactly how something worked at the startup of a SQL Server process running inside a docker container and my primary tool for this is stracewell how do you run strace against processes running in a container? I hadn’t done this before and needed to figure this out…so let’s go through how I pulled this off.

The First (not so successful) Attempt

My initial attempt involved creating a second container image with strace installed and then starting that container in the same PID namespace at the SQL Server container. The benefit here is that I do need to do anything special to the SQL Server container…I can use an unmodified SQL Server image and create a container for running strace.

Create a dockerfile for a container and install strace inside the container

FROM ubuntu:16.04

RUN export DEBIAN_FRONTEND=noninteractive && \
    apt-get update && \
    apt-get install -yq curl gnupg apt-transport-https && \
    apt-get install -y strace && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists

CMD /bin/bash

Then build the container with docker build -t strace .

docker build -t strace .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM ubuntu:16.04
 ---> a3551444fc85
Step 2/3 : RUN export DEBIAN_FRONTEND=noninteractive &&     apt-get update &&     apt-get install -yq curl gnupg apt-transport-https &&     apt-get install -y strace &&     apt-get clean &&     rm -rf /var/lib/apt/lists
 ---> Running in 2832df1c4921
Get:1 xenial-security InRelease [109 kB]
Get:2 xenial InRelease [247 kB]
...output omitted...
Fetched 179 kB in 0s (218 kB/s)
Selecting previously unselected package strace.
(Reading database ... 5300 files and directories currently installed.)
Preparing to unpack .../strace_4.11-1ubuntu3_amd64.deb ...
Unpacking strace (4.11-1ubuntu3) ...
Setting up strace (4.11-1ubuntu3) ...
Removing intermediate container 2832df1c4921
 ---> 686bc74ddd24
Step 3/3 : CMD /bin/bash
 ---> Running in 1b1ca2bb04d7
Removing intermediate container 1b1ca2bb04d7
 ---> d89cfe1231c1
Successfully built d89cfe1231c1
Successfully tagged strace:latest

With the container built let’s use it to run strace against our SQL Server process running in another container. 

Startup a container running SQL Server

docker run \
    --name 'sql19' \
    -p 1434:1433 \

Then start up our strace container and attach it to the PID namespace of the sql19 container. 

docker run -it \
    --cap-add=SYS_PTRACE \
    --pid=container:sql19 strace /bin/bash -c '/usr/bin/strace -f -p 1' 

A lot is going on in this command so let’s expand out each of the parameters

  • -it – this will attach the standard out of our container to our current shell. Basically, we’ll see the output of strace on our active console and can redirect to file if needed.
  • –cap-add=SYS_PTRACE – this adds the SYS_PTRACE capability to the container. This allows ptrace (the system call behind strace) the ability to attach to process. If this is not specified you will get an error saying ‘Operation not permitted’
  • –pid=container:sql19 – specifies the container and the namespace we want to attach to. This will start up our strace container in the same PID namespace as the sql19 container. With this there is one process namespace shared between the two containers, effectively they will be able to see each other’s processes which is what we want. We want the strace process to be able to see the sqlservr process.
  • strace – is the name of the container image we built above.
  • /bin/bash -c ‘/usr/bin/strace -p 1 -f’ – this is the command (CMD) we want to run inside the strace container. In this case, we’re starting a sh shell with the parameters to launch strace
  • strace -p 1 -f – the option  -p 1 will attach strace to PID 1 which is sqlservr and the -f option will attach to any forked processes from the traced process
 When we run this docker command we get this output
docker run -it    --cap-add=SYS_PTRACE    --pid=container:sql19 strace sh -c '/usr/bin/strace -p 1 -f'
/usr/bin/strace: Process 1 attached with 2 threads
[pid     9] ppoll([{fd=14, events=POLLIN}], 1, NULL, NULL, 8
[pid     1] wait4(10, 
We’re attaching to an already running docker container running SQL. But what we get is an idle SQL Server process this is great if we have a running workload we want to analyze but my goal for all of this is to see how SQL Server starts up and this isn’t going to cut it.
My next attempt was to stop the sql19 container and quickly start the strace container but the strace container still missed events at the startup of the sql19 container. So I needed a better way.
UPDATE: David Barbarin, fellow Data Platform MVP and SQL Server and Container expert, pursued the idea of using a second container and came up with a very elegant solution to this! He is using the sleep command at the launch of the SQL Server container then attaching a second strace container to the PID namespace. Using this technique he’s able to catch the startup events and not have to build a custom SQL Server container…check out the details here! Exactly what I’m looking for!
Also, as David points out in his post PID 1 is the watchdog process. I totally forgot about that in the code above. So when running the code above, swap -p 1 for the actual PID of the sqlservr process that is the child of PID 1. But a better way the is to use pgrep -P 1  to dynamical get the child process ID of PID 1.
So let’s use this technique to connect to the correct PID inside a running SQL Server container. This will attach to the child of PID 1, which will be the base sqlservr process that’s the database engine.
docker run -it \
    --cap-add=SYS_PTRACE \
    --pid=container:sql1 strace /bin/bash -c '/usr/bin/strace -f -p $(pgrep -P 1)' 
The Second (and more successful) Attempt

I want to attach strace to the SQL process at startup and the way that I can achieve that is by creating a custom container with SQL Server and strace installed. Then starting that container  telling strace to start up SQL Server process.

So let’s start by creating our custom SQL Server container with strace installed. Here’s the dockerfile for that

FROM ubuntu:16.04

RUN export DEBIAN_FRONTEND=noninteractive && \
    apt-get update && \
    apt-get install -yq curl gnupg apt-transport-https && \
    curl | apt-key add - && \
    curl | tee /etc/apt/sources.list.d/mssql-server.list && \
    apt-get update && \
    apt-get install -y mssql-server && \
 apt-get install -y strace && \     
apt-get clean && \ rm -rf /var/lib/apt/lists CMD /opt/mssql/bin/sqlservr

This is pretty standard for creating a SQL Server container the key difference here is that we’re installing the strace package in addition to the mssql-server package. Good news is, we can leave the CMD of the container as sqlservr…which means we can use this for general purpose database container as well as strace use cases. We’re going to use another technique to override CMD that when we start the container so that it will start a strace’d sqlservr process for us.

Let’s go ahead and build that container with docker build -t sqlstrace .

Sending build context to Docker daemon  127.1MB
Step 1/3 : FROM ubuntu:16.04
 ---> a3551444fc85
Step 2/3 : RUN export DEBIAN_FRONTEND=noninteractive &&     apt-get update &&     apt-get install -yq curl gnupg apt-transport-https &&     curl | apt-key add - &&     curl | tee /etc/apt/sources.list.d/mssql-server.list &&     apt-get update &&     apt-get install -y mssql-server &&     apt-get install -y strace &&     apt-get clean &&     rm -rf /var/lib/apt/lists
 ---> Running in 806a3b4b9345
Get:1 xenial-security InRelease [109 kB]
...output omitted...
Setting up mssql-server (15.0.1900.25-1) …
...output omitted...
 ---> 42a1ca28ae72
Step 3/3 : CMD /opt/mssql/bin/sqlservr
 ---> Running in 1e57d6759df6
Removing intermediate container 1e57d6759df6
 ---> 6e3f5e82a177
Successfully built 6e3f5e82a177
Successfully tagged sqlstrace:latest

Once that container is built we can override the CMD that’s used to start the container defined in the dockerfile with another executable inside the container…you guessed it, strace.

docker run \
    --name 'sql19strace'  -it  \
    -p 1433:1433 \
     sqlstrace /bin/bash -c "/usr/bin/strace -f /opt/mssql/bin/sqlservr"

The first four lines of the docker run command are standard for starting a SQL Server container. But that last line is a bit different, we’re starting our sqlstrace container. Inside that container image we’re starting a bash shell and passing in the command (-c“/usr/bin/strace -f /opt/mssql/bin/sqlservr” which will start strace, following any forked processes (-f) and then start SQL Server (sqlservr). From there SQL Server will start up and strace will have full visibility into the process execution.  The cool thing about this technique is we can adjust our strace parameters as needed at the time we create the container. 

execve("/opt/mssql/bin/sqlservr", ["/opt/mssql/bin/sqlservr"], [/* 9 vars */]) = 0
brk(NULL)                               = 0x55b7bc77c000
access("/etc/", F_OK)      = -1 ENOENT (No such file or directory)
readlink("/proc/self/exe", "/opt/mssql/bin/sqlservr", 4096) = 23
access("/etc/", R_OK)      = -1 ENOENT (No such file or directory)
open("/opt/mssql/bin/tls/x86_64/", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/opt/mssql/bin/tls/x86_64", 0x7fffe9bc9510) = -1 ENOENT (No such file or directory)
open("/opt/mssql/bin/tls/", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/opt/mssql/bin/tls", 0x7fffe9bc9510) = -1 ENOENT (No such file or directory)
open("/opt/mssql/bin/x86_64/", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
...output omitted... 

Above is the output of strace on SQL Server kicking off with an execve which is the system call used after a fork to swap in the new program into the new process space. 

Hopefully, this can help you get into those deep dive debugging/troubleshooting/discovery scenarios you may find yourself working with in SQL Server inside a container

Docker Image Tags are Case Sensitive

A quick post about pulling docker containers (this applies to docker run too)…when specifying the container image, the container image name and tag are case sensitive. We’re not going to discuss how much time troubleshooting it too me to figure this out…but let’s just say it’s more than I care to admit publicly. 

In this code you can see I’m specifying the following image and tag server:2019-rc1-ubuntu (notice the lowercase rc in the tag)

docker pull 

Docker responds that it cannot find that image manifest

Error response from daemon: manifest for not found: manifest unknown: manifest unknown 

If we specify server:2019-RC1-ubuntu (notice the uppercase RC in the tag)

docker pull

Then docker is able to find that image and downloads it to my local machine

2019-RC1-ubuntu: Pulling from mssql/server
59ab41dd721a: Already exists
57da90bec92c: Already exists
06fe57530625: Already exists
5a6315cba1ff: Already exists
739f58768b3f: Already exists
e39f945bda21: Pull complete
6689ce95f395: Pull complete
ec004dcfdfb5: Pull complete
e44708601d04: Pull complete
Digest: sha256:a11facbda1b1cc299d4a37499ef79cd18e38bfb8e5dd7e45cc73670cc07772e5
Status: Downloaded newer image for

Want to get a list of tags for a container image so you know what image and tags to specify? Here’s how you do that with curl.

 curl -L 

If you’re of the PowerShell persuasion (shout out to Andrew Pruski for this gem) here how you can generate a list of tags with Invoke-Webrequest


Persisting SQL Server Data in Docker Containers – Part 3

In the first two posts in this series we discussed the need for data persistency in containers then we discussed where the data actually lives on our systems. Now let’s look at specifying the location of the data on the underlying file system of the base OS. 

This is the third post in a three part series on Persisting SQL Server Data in Docker Containers. The first post introducing Docker Volumes is hereThe second post on where Docker actually stores your data is here.

Exposing Directories on the Base OS into a Container

Now what if I wanted to expose a directory from my base OS, macOS directly into the container avoiding placing my data inside the Docker Linux VM. Let’s try it and see what happens…let’s start up a container with a Docker Volume mapping /Users/demo/demos/data on the base OS into the container at /var/opt/mssql.

docker run \
    --name 'sql19dv' \
    -p 1433:1433 \
    -v /Users/demo/demos/data:/var/opt/mssql \

If we do a docker ps -a we’ll find our conainer existed with a non-zero exit code. That’s bad.

CONTAINER ID        IMAGE                                        COMMAND                  CREATED             STATUS                        PORTS                    NAMES
2f4a9efc2f89   "/opt/mssql/bin/perm…"   21 seconds ago      Exited (1) 11 seconds ago                              sql19dv

The first thing you should do when this happens is to examine the container’s logs. We can do that with docker logs sql19dv (where sql19dv is our container name) and we’ll get this output.

This program has encountered a fatal error and cannot continue running at Sun Sep  1 14:19:06 2019
The following diagnostic information is available:

         Reason: 0x00000006
        Message: Kernel bug check
        Address: 0x6b047d50
     Parameters: 0x10861f590
    Stack Trace:
        Process: 9 - sqlservr
         Thread: 13 (application thread 0x4)
    Instance Id: 866b1bc1-211d-4390-aa43-a48b32d6f78e
       Crash Id: 07c2a35d-5ddf-4e5d-ad69-a91ef0f5d0e9
    Build stamp: 228a531f7a324b94dd3127e706f889b081f5677bd368be8b30485d8edda4d02b
   Distribution: Ubuntu 16.04.6 LTS
     Processors: 6
   Total Memory: 6246559744 bytes
      Timestamp: Sun Sep  1 14:19:06 2019
     Last errno: 2
Last errno text: No such file or directory

Ubuntu 16.04.6 LTS
Capturing core dump and information to /var/opt/mssql/log...
dmesg: read kernel buffer failed: Operation not permitted No journal files were found.
No journal files were found.
Sun Sep  1 14:19:07 UTC 2019 Capturing program information
Sun Sep  1 14:19:08 UTC 2019 Attempting to capture a dump with paldumper
Captured a dump with paldumper
Sun Sep  1 14:19:11 UTC 2019 Capturing program binaries
Sun Sep  1 14:19:12 UTC 2019 Compressing the dump files

In the output above, SQL Server crashes trying to access the file inside the container that isn’t there…see the ‘Last errno text’ using strace inside the container yields the following information. 

[pid    11] lstat("/opt/mssql/lib/system", 0x7f8f15b63350) = -1 ENOENT (No such file or directory)
[pid    11] <... lstat resumed> 0x7f8f15b63350) = -1 ENOENT (No such file or directory)

Let’s Map a Different Path and See What Happens

docker run \
    --name 'sql19dv2' \
    -p 1432:1433 \
    -v /Users/demo/demos/data:/var/opt/mssql/data \

Here we’re mapping to /var/opt/mssql/data (where above we mapped /var/opt/mssql). This mapping will fail and the container won’t start but this time for a different reason.

If we look at  docker logs you’ll find the following error in from the SQL Server Error Log.

2019-09-02 18:10:15.08 Server      Error 87(The parameter is incorrect.) occurred while opening file '/var/opt/mssql/data/master.mdf' to obtain configuration information at startup. An invalid startup option might have caused the error. Verify your startup options, and correct or remove them if necessary.

Makes sense…we changed where SQL Server is reading/writing data. macOS doesn’t support a file mode called O_DIRECT which allows for unbuffered read/write access to the file opened using the open system call.  O_DIRECT is used by systems that manage their own file caching, like relational database management systems (RDBMS). So as SQL starts up and tries to open the master database with O_DIRECT the files can’t be opened because the macOS kernel doesn’t support this mode. And this is the reason why we have to have that Linux VM around. That Linux VM will support O_DIRECT option on the file opened. See more about this at the GitHub issue here.

An strace of the thread shows the following:

open("/var/opt/mssql/data/master.mdf", O_RDONLY|O_DIRECT
<... open resumed> )        = -1 EINVAL (Invalid argument)
...output omitted...
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_ACCERR, si_addr=0xb804b1fe8} —
...the remainder of the strace shows the creation of the SQL Server Error Log record above...

This issue is specific to macOS. On a Linux machine it would map the base OS directory directly into the container and the file operations will work because the kernel supports the correct file modes on the open system call. On a Window machine this works as confirmed by my friend and fellow MVP Andrew Pruski

But we still can use our base OS directories…really!

All isn’t lost if you’re running Linux containers on a Mac and need to run SQL Server to access the base OS’s file system. We can still use Docker Volumes for other parts of the container. Let’s create a container using TWO Docker Volumes. Let’s define sqldata1 as using the file system inside the Docker VM and we’ll define a second Docker Volume that we can use to read/write other information…like backups. 

 docker run \
    --name 'sql19dv1' \
    -p 1433:1433 \
    -v sqldata1:/var/opt/mssql \
    -v /Users/demo/demos/backup:/backup \

In this configuration our SQL instance will use sqldata1 mapped to /var/opt/mssql for its instance data directories inside the Docker Linux VM so SQL is able to open the files with the appropriate file modes. But we can still read/write information directly to our base OS in the directory /Users/demo/demos/backup which is mapped into the container at the location /backup. Backup files do not use the O_DIRECT flag.

Now let’s run a backup of our database to that location.

sqlcmd -S localhost,1433 -U sa -Q "BACKUP DATABASE [TestDB1] TO DISK = '/backup/TestDB1.bak'" -P $PASSWORD -W

And if we look at that directory on the base operating system we’ll see the databases backup outside the container. That’s cool. Now your automatic backups of your workstations can pick up that file and back it up into the cloud for you…right?

ls -la /Users/demo/demos/backup
total 6504
drwxr-xr-x  3 demo  staff       96 Sep  1 10:04 .
drwxr-xr-x  5 demo  staff      160 Sep  1 09:48 ..
-rw-r-----  1 demo  staff  3330048 Sep  1 10:03 TestDB1.bak

Let’s do something cool…

We can share that /backup volume with other containers on our system. With the container sql19dv1 still running we can start up another container, sql19dv2. We’ll need to ensure this container has a unique name, unique port to listen on and a unique Volume for the instance’s files. The only thing it’s going to share is the backup volume. This technique isn’t specific to containers macOS. This will work on Windows and Linux as well.

 docker run \
    --name 'sql19dv2' \
    -p 1432:1433 \
    -v sqldata2:/var/opt/mssql \
    -v /Users/demo/demos/backup:/backup \

With this container running we can execute a RESTORE statement on the backups that are on the base OS at /Users/demo/demos/backup and mapped into the container at /backup. This technique can be effective if you’re using larger data sets avoiding having to copy the backup into the container with docker cp. 

sqlcmd -S localhost,1432 -U sa -Q "RESTORE DATABASE [TestDB1] FROM DISK = '/backup/TestDB1.bak'" -P $PASSWORD -W
Processed 392 pages for database 'TestDB1', file 'TestDB1' on file 1.
Processed 2 pages for database 'TestDB1', file 'TestDB1_log' on file 1.
RESTORE DATABASE successfully processed 394 pages in 0.026 seconds (118.239 MB/sec).

Wrapping things up 

In this post, we introduced being able to map a file location from the base OS into a container and use it for reading and writing data, in our examples backup files. This could be any type of data. We also learned that for SQL Server data files we still need to use the Docker Volume that’s serviced by the Linux container. We also learned how we can share a Docker Volume between containers a quick way to move backups and other data between containers without having to use docker cp

This technique isn’t specific to containers macOS. This will work on Windows and Linux as well.

Persisting SQL Server Data in Docker Containers – Part 2

So in my previous post, we discussed Docker Volumes and how they have a lifecycle independent of the container enabling us to service the container image independent of the data inside the container. Now let’s dig into Volumes a little bit more and learn where Docker actually stores that data on the underlying operating system.  

This is the second post in a three part series on Persisting SQL Server Data in Docker Containers. The first post introducing Docker Volumes is here. And the third post on mapping base OS directories directly into containers is here.

Docker Volumes

When we working with Docker Volumes we can use the -v option to create the Volume at the time the container is created or we can create the Volume ahead of time with the docker volume create command. The left side sqldata1 is the Volume name. The right side /var/opt/mssql is the directory the Volume will be mapped to inside the container. 

We can specify the location of the Volume on the underlying OS but if we don’t it will be created in the default location defined in your Docker preferences which is /var/lib/docker/volumes. Let’s go with the default location for now…we’re going to cover specifying locations in an upcoming post.

docker run \
    --name 'sql19' \
    -p 1433:1433 \
    -v sqldata1:/var/opt/mssql \

On a system where Linux is the base OS running Docker the container runtime will create Volumes on the base OS’s file system and map them into the container. So as we create Volumes, they will land in /var/lib/docker/volumes when using the code above that doesn’t specify a specific file location on the base OS. Well, /var/lib/docker/volumes is quite a Linux’y sounding path and if I try to browse to that path on my Mac or Windows it doesn’t exist. OK, I don’t have a Windows system but I think it’s a safe bet that’s not there either :)

Let’s use the inspect command to get more information on a Docker resource and here we’re using docker volume inspect on sqldata1.

docker volume inspect sqldata1
        "CreatedAt": "2019-09-01T11:51:25Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/sqldata1/_data",
        "Name": "sqldata1",
        "Options": null,
        "Scope": "local"

In the output here we can see the Mountpoint, which is where the data lives on the underlying OS, as /var/lib/docker/volumes/sqldata1/_data that’s where Docker put the Volume since I didn’t define a path it put the Volume data in the default location of /var/lib/docker/volumes…but like we said a second ago that path doesn’t exists on my local filesystem…what’s going on?

Docker Volumes on non-Linux Operating Systems

Docker on Mac (and Windows uses the same technique for now) exposes a small Linux VM to provide kernel services to Linux containers. When we create a Docker Volume without specifying a file location for the Volume it will be created *inside* this VM. So that’s where our Volume data is stored. Let’s keep digging…

Using the technique defined here we can get a shell into that Virtual Machine and browse the file system to find the actual data files. Here’s how you attach a shell to the Docker VM.

screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty

Now let’s decompose this command below a bit…screen is a command used to attach a shell to a process, the path (~/Library/Containers/com.docker.docker/Data/vms/0/tty) is the default disk image path defined in your Docker preferences. And the file tty is a process running that exposes a shell interface to the running VM supporting our Linux containers. Everything in Linux is a file, so that tty is actually a terminal process inside the VM that’s exposed outside the VM on your base operating system’s file system. Screen will ‘open’ that file and start to read from it…since it’s exposes a terminal you get a shell into the VM. 

Now that we’re inside the VM let’s look around and see where our Volume data lives, we see all of the actual SQL Server file data supporting the instance and the databases and log files.

ls /var/lib/docker/volumes/
metadata.db sqldata1

In the directory listing above we’ll see a directory for each named Docker Volume in this case we see sqldata1.

ls /var/lib/docker/volumes/sqldata1/_data/
data     log      secrets

Inside of sqldata1, we will start to see the directories and files created by SQL Server in the Docker Volume. Remember SQL Server thinks this is /var/opt/mssql because this path is being mapped into the container at that location.  

ls /var/lib/docker/volumes/sqldata1/_data/data/
Entropy.bin                     model.mdf
MS_AgentSigningCertificate.cer  modellog.ldf
TestDB1.mdf                     msdbdata.mdf
TestDB1_log.ldf                 msdblog.ldf
master.mdf                      tempdb.mdf
mastlog.ldf                     templog.ldf

And if we look inside the data directory we’ll see system and user databases. 

What’s Next? 

So now that we know where Docker Volume data lives when running Linux Containers on a Mac (this same technique applies to Windows too), in our next post we’re going to go a little bit further into this and define a specific location for Docker to store our data on the base OS.

Persisting SQL Server Data in Docker Containers – Part 1

What’s the number one thing a data professional wants to do with their data…keep it around. Let’s talk about running SQL Server in Containers using Docker Volumes on a Mac

This is the first post in a three part series on Persisting SQL Server Data in Docker Containers. The second post on where Docker actually stores your data is here. And the third post on mapping base OS directories directly into containers is here.

The Need for Data Persistency in Containers

A container image is read-only. When an application changes data inside a running container writes are written to a writable layer. The writable layer plus the read-only container image are brought together by the container runtime and presenting to the processes running inside the container as a single file system. 

The primary issue with this is that the writeable layer has the lifecycle of the container. If you delete the container, you delete the writeable layer and any data that was in there. Luckily Docker containers give us a way to decouple the container and its data.

In Figure 1, you can see a container image and it’s writeable layer. The application inside the container sees this has a single file system. If we delete this container, any data written to the writeable layer will be deleted too. 


Figure 1: A container and it’s writable layer

Docker Volumes

A Docker Volume is a Docker managed resource that is mapped into a defined point in the filesystem inside the container. The primary benefit of using Docker Volumes is that they have a lifecycle that’s independent of a container. This enables you to decouple your application from its state to the point where you can simply throw away the container and replace it with a new container image start up your application and point it to your data.

In Figure 2, we have a container, a writeable layer and a volume. A container will always have a writeable layer even when a Volume is defined. A Volume will be mounted at a specific location in the file system inside the container and writes to that location will be written to the Volume. Writes to other parts of the file system will be written to the writable layer. 


Figure 2: A container, it’s writable layer and a Volume

SQL Server using Docker Volumes

Let’s talk about how we can use Docker Volumes and SQL Server to persist data. If we want to run SQL Server in a container we will want to decouple our data from the container itself. Doing so will enable us to delete the container, replace it and start up a new one pointing at our existing data. When running SQL Server in a container will store data in /var/opt/mssql by default. When the container starts up for the first time it will put the system databases in that location and any user databases created will also be placed at this location by default. 

Now, if we don’t use a Volume that data will be written into the writeable layer of the container and if we delete the container…we delete our data. We don’t want that so let’s start up a container with a Volume. To do so we use the -v option when we use the docker run command. 

docker run \
    --name 'sql17' \
    -p 1433:1433 \
    -v sqldata1:/var/opt/mssql \

In the code above you can see –v sqldata1:/var/opt/mssql specified as part of the docker run command. This creates a Docker Volume sqldata1 and maps that inside the container to /var/opt/mssql. Now during this container’s start up when SQL Server will write its data to /var/opt/mssql which is actually going to be written to the Volume. If we delete this container and replace it…when SQL Server starts up it will see the master database and proceed initializing the system as defined in master. If there are any user databases defined in master and they’re accessible they will be brought online too. Let’s try it out…first up let’s create a user database and query the file information about the databases in this container.

sqlcmd -S localhost,1433 -U sa -Q 'CREATE DATABASE TestDB1' -P $PASSWORD

sqlcmd -S localhost,1433 -U sa -Q 'SELECT name, physical_name from sys.master_files' -P $PASSWORD -W

name physical_name
---- -------------
master /var/opt/mssql/data/master.mdf
mastlog /var/opt/mssql/data/mastlog.ldf
tempdev /var/opt/mssql/data/tempdb.mdf
templog /var/opt/mssql/data/templog.ldf
modeldev /var/opt/mssql/data/model.mdf
modellog /var/opt/mssql/data/modellog.ldf
MSDBData /var/opt/mssql/data/MSDBData.mdf
MSDBLog /var/opt/mssql/data/MSDBLog.ldf
TestDB1 /var/opt/mssql/data/TestDB1.mdf
TestDB1_log /var/opt/mssql/data/TestDB1_log.ldf

In the code above we create a database, when query master for the information about the databases running inside this container. You can see all of the paths are /var/opt/mssql which is our volume. 

Container and Data Independence

The Docker Volume created with the -v option created a Docker managed Volume that is independent of the container so our data will live in there and we can service the container independent of the volume. So let’s do that…let’s delete the container and start up a new container and let’s go so far as to use a 2019 container to upgrade SQL Server…that’s cool!

docker stop sql17
docker rm sql17

The code above will stop and then delete the container. When the container is deleted, so is its writeable layer. But we are storing out data in the Volume and that still exists.

docker volume ls
local               sqldata1

Above we can see there is a Volume using the local driver and its name is sqldata1…this still exists and can be mounted by new containers. The local drive is used to map directories from the base OS inside the container. There are other types of drivers that expose other types of storage into the container. More on this later.

docker run \
    --name 'sql19' \
    -p 1433:1433 \
    -v sqldata1:/var/opt/mssql \

With this code, we start up a new container and tell it to use the same Volume and mount it into /var/opt/mssql. So when SQL Server starts it finds the master database, master has the metadata about any configuration and user databases and we get back into the state we were previously in. Let’s ask SQL Server for a list of databases.

sqlcmd -S localhost,1433 -U sa -Q 'SELECT name, physical_name from sys.master_files' -P $PASSWORD -W

name physical_name
---- -------------
master /var/opt/mssql/data/master.mdf
mastlog /var/opt/mssql/data/mastlog.ldf
tempdev /var/opt/mssql/data/tempdb.mdf
templog /var/opt/mssql/data/templog.ldf
modeldev /var/opt/mssql/data/model.mdf
modellog /var/opt/mssql/data/modellog.ldf
MSDBData /var/opt/mssql/data/MSDBData.mdf
MSDBLog /var/opt/mssql/data/MSDBLog.ldf
TestDB1 /var/opt/mssql/data/TestDB1.mdf
TestDB1_log /var/opt/mssql/data/TestDB1_log.ldf

…and there you can see in the output above, SQL Server is in the state it was in the initial running of the container on the 2017 image. Now we’re on the 2019 image and have access to all of our persisted data independent of the container image. 

sqlcmd -S localhost,1433 -U sa -Q 'SELECT @@VERSION' -P $PASSWORD
Microsoft SQL Server 2019 (RC1) - 15.0.1900.25 (X64)
        Aug 16 2019 14:20:53
        Copyright (C) 2019 Microsoft Corporation
        Developer Edition (64-bit) on Linux (Ubuntu 16.04.6 LTS)                                                                                                                       

Containers have replaced virtual machines for me and the decoupling of data and computation will have a significant impact on how we design data platforms and systems going forward. In this post, I wanted to highlight how you can use a container with persistent state systems like SQL Server. In the next post, I’m going to show you where that data actually lives on the underlying Operating System.