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' \
    -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD='$PASSWORD \
    -p 1433:1433 \
    -v /Users/demo/demos/data:/var/opt/mssql \
    -d mcr.microsoft.com/mssql/server:2019-latest

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        mcr.microsoft.com/mssql/server:2019-latest   "/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:
                 000000006b13542e
                 000000006b047dab
                 000000006b03447e
                 000000006b043025
                 000000006b0431f6
                 000000006b1336fc
                 000000006b13226f
                 000000006b175661
        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' \
    -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD='$PASSWORD \
    -p 1432:1433 \
    -v /Users/demo/demos/data:/var/opt/mssql/data \
    -d mcr.microsoft.com/mssql/server:2019-latest

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' \
    -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD='$PASSWORD \
    -p 1433:1433 \
    -v sqldata1:/var/opt/mssql \
    -v /Users/demo/demos/backup:/backup \
    -d mcr.microsoft.com/mssql/server:2019-latest

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' \
    -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD='$PASSWORD \
    -p 1432:1433 \
    -v sqldata2:/var/opt/mssql \
    -v /Users/demo/demos/backup:/backup \
    -d mcr.microsoft.com/mssql/server:2019-latest

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.