Docker image layers lessons learned

Common misconception that deleting files during docker build at later image layers reducing final image size. For example if you have Dockerfile like below, you would think that total image size will be similar to base image since we deleted downloaded file at later stage of image build.

FROM mcr.microsoft.com/windows/nanoserver:1809
ADD ["http://ipv4.download.thinkbroadband.com/100MB.zip", "."]
RUN del 100mb.zip

To see effect on deletion had on final image size it’s good to start with just ADD statement in dockerfile

FROM mcr.microsoft.com/windows/nanoserver:1809
ADD ["http://ipv4.download.thinkbroadband.com/100MB.zip", "."]

Resulting build is in fact showing that image size increased by 100 MB compared to base image like below 

PS C:\docker\LayerTest> docker build -t layers:delete .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM mcr.microsoft.com/windows/nanoserver:1809
 ---> a5034827da99
Step 2/3 : ADD ["http://ipv4.download.thinkbroadband.com/100MB.zip", "."]
Downloading [==================================================>]  104.9MB/104.9MB
 ---> Using cache
 ---> feba369ecb4e
Step 3/3 : RUN del 100mb.zip
 ---> Running in 3307758a70ce
Removing intermediate container 3307758a70ce
 ---> 74d6679e81cf
Successfully built 74d6679e81cf
Successfully tagged layers:delete
PS C:\docker\LayerTest> docker images
REPOSITORY                             TAG                 IMAGE ID            CREATED             SIZE
layers                                 add                 feba369ecb4e        48 seconds ago      410MB
mcr.microsoft.com/windows/nanoserver   1809                a5034827da99        2 weeks ago         305MB

As expected image increased by about 100 MB with addition of the file. Now let’s try to delete that file with `RUN DEL 100MB.zip`. Common misconception is that this will remove file inside image and hence total size will decrease back to original base image size. The results are below.

FROM mcr.microsoft.com/windows/nanoserver:1809
ADD ["http://ipv4.download.thinkbroadband.com/100MB.zip", "."]
RUN del 100mb.zip

Results of the build below which are showing that not only final image size did not decrease but it’s in fact increased by 1 MB!

PS C:\docker\LayerTest> docker build -t layers:delete .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM mcr.microsoft.com/windows/nanoserver:1809
 ---> a5034827da99
Step 2/3 : ADD ["http://ipv4.download.thinkbroadband.com/100MB.zip", "."]
Downloading [==================================================>]  104.9MB/104.9MB
 ---> Using cache
 ---> feba369ecb4e
Step 3/3 : RUN del 100mb.zip
 ---> Running in 3307758a70ce
Removing intermediate container 3307758a70ce
 ---> 74d6679e81cf
Successfully built 74d6679e81cf
Successfully tagged layers:delete
PS C:\docker\LayerTest> docker images
REPOSITORY                             TAG                 IMAGE ID            CREATED             SIZE
layers                                 delete              74d6679e81cf        4 seconds ago       411MB
layers                                 add                 feba369ecb4e        48 seconds ago      410MB
mcr.microsoft.com/windows/nanoserver   1809                a5034827da99        2 weeks ago         305MB

You can see what was done on image layers with docker history <imgid> command like below, showing that last layer did not change anything but added 1 MB to total size.

PS C:\docker\LayerTest&gt; docker history 74
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
74d6679e81cf        17 minutes ago      cmd /S /C del 100mb.zip                         1.05MB
feba369ecb4e        17 minutes ago      cmd /S /C #(nop) ADD aa41da93b6f56103ecbc3fd…   105MB
a5034827da99        3 weeks ago         Install update 1809_amd64                       61.6MB
<missing&gt;           2 months ago        Apply image 1809_RTM_amd64                      244MB

The take out of this is that you can not decrease total size of the image in any following layers, you can only INCREASE it. That means you need to keep each created layer as small possible by cleaning up file inside the same RUN statement. Example above can be rewritten as below where build process downloads files and then deletes it. Since it’s done inside single layer total size of image does not change.

FROM mcr.microsoft.com/windows/nanoserver:1809
RUN curl http://ipv4.download.thinkbroadband.com/100MB.zip --output 100MB.zip &\
    del 100MB.zip
PS C:\docker\LayerTest> docker build -t layers:curl .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM mcr.microsoft.com/windows/nanoserver:1809
 ---> a5034827da99
Step 2/2 : RUN curl http://ipv4.download.thinkbroadband.com/100MB.zip --output 100MB.zip &    del 100MB.zip
 ---> Running in 58026752da9e
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  100M  100  100M    0     0  6023k      0  0:00:17  0:00:17 --:--:-- 7782k
Removing intermediate container 58026752da9e
 ---> b9de8a8a5077
Successfully built b9de8a8a5077
Successfully tagged layers:curl
PS C:\docker\LayerTest> docker images
REPOSITORY                             TAG                 IMAGE ID            CREATED             SIZE
layers                                 curl                b9de8a8a5077        7 seconds ago       307MB
layers                                 delete              74d6679e81cf        15 minutes ago      411MB
layers                                 add                 feba369ecb4e        16 minutes ago      410MB
mcr.microsoft.com/windows/nanoserver   1809                a5034827da99        2 weeks ago         305MB

This is especially important if you use MSI/EXE based installation in full servercore image since MSI based installer leave uninstallation/reinstallation files behind which inflate image size unneccessary. Compare common scenario below. You add MSI package to your image, you run installation, you delete MSI installer. Example below adds MariaDB installation package to base servercore image, installs it and then deletes installation file. Which is all wrong based on discussion above.

FROM mcr.microsoft.com/windows/servercore:ltsc2019
WORKDIR prep
ADD ["https://downloads.mariadb.com/Bundles/TX/mariadb-tx-3.0-10.3.11-windows.zip", "mariadb.zip"]
RUN powershell -Command Expand-Archive mariadb.zip
RUN powershell -command "Start-Process -filepath 'msiexec' -ArgumentList @('/i', 'c:\prep\mariadb\mariadb-tx-3.0-10.3.11-windows\mariadb-10.3.11-winx64.msi', '/qn') -PassThru | wait-process"
RUN del /f /s /q .

Resulting image history showing up that each layer significantly increase total image size.

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
d26180fd56b8        25 seconds ago      cmd /S /C del /f /s /q .                        5.22MB
17b2bad012a1        3 minutes ago       cmd /S /C powershell -command "Start-Process…   482MB
bd9161ad3507        8 minutes ago       cmd /S /C powershell -Command Expand-Archive…   99.3MB
dd3ccba3a5ff        9 minutes ago       cmd /S /C #(nop) ADD 7d838d807796b908c08ade6…   71.8MB
94bfd0c4d09f        12 minutes ago      cmd /S /C #(nop) WORKDIR C:\prep                41kB
670f5c41d658        3 weeks ago         Install update ltsc2019_amd64                   509MB
<missing&gt;           2 months ago        Apply image 1809_RTM_amd64                      3.47GB

Better version of the same process to confine entire process to single layer like below

FROM mcr.microsoft.com/windows/servercore:ltsc2019
WORKDIR prep
RUN curl "https://downloads.mariadb.com/Bundles/TX/mariadb-tx-3.0-10.3.11-windows.zip" --output mariadb.zip &  \
    powershell -command "Expand-Archive mariadb.zip" & \
    powershell -command "Start-Process -filepath 'msiexec' @('/i', 'c:\prep\mariadb\mariadb-tx-3.0-10.3.11-windows\mariadb-10.3.11-winx64.msi', '/qn') -PassThru | Wait-Process" & \
    del /f /s /q . & \

Resulting image showing that final image size of installation process decreased from 661 MB to 447 MB

IMAGE               CREATED              CREATED BY                                      SIZE                COMMENT
e727a4dd3642        About a minute ago   cmd /S /C curl "https://downloads.mariadb.co…   447MB
94bfd0c4d09f        18 minutes ago       cmd /S /C #(nop) WORKDIR C:\prep                41kB
670f5c41d658        3 weeks ago          Install update ltsc2019_amd64                   509MB
<missing&gt;           2 months ago         Apply image 1809_RTM_amd64                      3.47GB

Now back to notice that MSI installer always leaves cleanup binaries behind. This binaries are located in c:\windows\installer folder. You can verify that they are there by checking contents of that folder inside better image like below.

PS C:\docker\LayerTest> docker run --rm e7 cmd /c dir c:\windows\installer
 Volume in drive C has no label.
 Volume Serial Number is 069E-146F

 Directory of c:\windows\installer

11/16/2018  09:05 PM        54,956,032 6ee9.msi
12/02/2018  05:00 PM            20,480 SourceHash{D02C77A8-80E6-4CA2-8028-BC2AF9BE21B1}
               2 File(s)     54,976,512 bytes

This files can deleted since no uninstallation will ever be performed inside docker. So final Dockerfile will look like below which results in extra 55MB savings.

FROM mcr.microsoft.com/windows/servercore:ltsc2019
WORKDIR prep
RUN curl "https://downloads.mariadb.com/Bundles/TX/mariadb-tx-3.0-10.3.11-windows.zip" --output mariadb.zip &  \
    powershell -command "Expand-Archive mariadb.zip" & \
    powershell -command "Start-Process -filepath 'msiexec' @('/i', 'c:\prep\mariadb\mariadb-tx-3.0-10.3.11-windows\mariadb-10.3.11-winx64.msi', '/qn') -PassThru | Wait-Process" & \
    del /f /s /q . & \
    del /f /s /q c:\windows\installer\*

By optimizing image build we went from 661MB file to 392MB with no change in functionality

Advertisements

Using pure powershell to generate TLS certificates for Docker daemon running on Windows

Steps below will allow you to create necessary PKI infrastructure to secure you docker daemons with no requirement to download any external tools. Snippets below are not part of complete script but tidbits which you can use to procure both CA, server certificate as well as client certificate and reuse pieces for issuing additional certificates down the road.

Docker daemon requires 3 files on server to for secure TLS connection:

  • tlscacert which is Base64 encoded public key of CA certificate
  • tlscert which is Base64 encoded public key of server certificate
  • tlskey which Base64 encoded private key of server certificate

Docker daemon will list those keys in file called daemon.json under  $env:programdata\docker\config

Example of that file is below

{
"group": "Network Service",
"graph": "E:\\images",
"tlscacert": "C:\\ProgramData\\docker\\certs.d\\rootCA.cer",
"tlskey": "C:\\ProgramData\\docker\\certs.d\\privateKey.cer",
"hosts": [
"tcp://0.0.0.0:2376",
"npipe://"
],
"tlscert": "C:\\ProgramData\\docker\\certs.d\\serverCert.cer",
"tlsverify": true
}

Similar files will be required on client to connect to server, difference is certificate which will be used to connect which will have different EKU (Enhanced Key Usage) specified

  • tlscacert which is Base64 encoded public key of CA certificate
  • tlscert which is Base64 encoded public key of client certificate
  • tlskey which is Base64 encoded private key of client certificate
Docker client will use syntax below to connect to TLS secured docker endpoint
& docker --tlsverify --tlscacert=c:\test\rootca.cer --tlscert=c:\test\clientPublicKey.cer --tlskey=c:\test\clientPrivateKey.cer -H=tcp://containerhost1:2376 version

Creating CA certificate

Snippet below creates CA certificate and exports it’s public key to c:\test\rootCA.cer. Private key stays in your Windows Certificate Store and is exportable for your backup purpouses and reissuing new server and client certificates later. The only changeable parameter which you can modify for your environment is Subject.

        $splat = @{
        type = "Custom" ;
        KeyExportPolicy = "Exportable";
        Subject = "CN=Docker TLS Root";
        CertStoreLocation = "Cert:\CurrentUser\My";
        HashAlgorithm = "sha256";
        KeyLength = 4096;
        KeyUsage = @("CertSign", "CRLSign");
        TextExtension = @("2.5.29.19 ={critical} {text}ca=1")
    }
    $rootCert = New-SelfSignedCertificate @splat

After CA certificate is generated we need to export it’s public key to a file, the only changeable part here is Path

 $splat = @{
Path = "c:\test\rootCA.cer";
        Value = "-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($rootCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----";
        Encoding = "ASCII";
    }
    Set-Content @splat

Creating Server Certificate to secure TLS on container host

Code similar to generation of CA certificate with few notable changes, that is we provide which certificate is used to sign it as well as type of certificate, we export key after cert is generated. Changeable parameters are DNSName and Path

    $splat = @{
        CertStoreLocation = "Cert:\CurrentUser\My";
        DnsName = "swarmmanager1", "localhost", "containerhost1";
        Signer = $rootCert ;
        KeyExportPolicy = "Exportable";
        Provider = "Microsoft Enhanced Cryptographic Provider v1.0";
        Type = "SSLServerAuthentication";
        HashAlgorithm = "sha256";
        TextExtension = @("2.5.29.37= {text}1.3.6.1.5.5.7.3.1");
        KeyLength = 4096;
    }
    $serverCert = New-SelfSignedCertificate @splat
    $splat = @{
       Path = "c:\test\serverCert.cer";
       Value = "-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($serverCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----";
       Encoding = "Ascii"
}
   Set-Content @splat

Exporting private key for server certificate to a file

Last step for TLS connectivity for docker host to export private key to Base64 encoded file which has been pretty difficult with off the shelf powershell/.NET framework untill version 4.6 which provided method to export that key. Implementation is below

    $privateKeyFromCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($serverCert)
    $splat = @{
        Path = "c:\test\privateKey.cer";
        Value = ("-----BEGIN RSA PRIVATE KEY-----`n" + [System.Convert]::ToBase64String($privateKeyFromCert.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob), [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END RSA PRIVATE KEY-----");
        Encoding = "Ascii";
    }
    Set-Content @splat

Creating client certificate

Code below performs similar tasks in relevant to client certificate like server certificate tasks above

    $splat = @{
        CertStoreLocation = "Cert:\CurrentUser\My";
        Subject = "CN=clientCert";
        Signer = $rootCert ;
        KeyExportPolicy = "Exportable";
        Provider = "Microsoft Enhanced Cryptographic Provider v1.0";
        TextExtension = @("2.5.29.37= {text}1.3.6.1.5.5.7.3.2") ;
        HashAlgorithm = "sha256";
        KeyLength = 4096;
    }
    $clientCert = New-SelfSignedCertificate  @splat
    $splat = @{
        Path = "c:\test\clientPublicKey.cer" ;
        Value = ("-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($clientCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----");
        Encoding = "Ascii";
    }
    Set-Content  @splat
    $clientprivateKeyFromCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($clientCert)
    $splat = @{
        Path = "c:\test\clientPrivateKey.cer";
        Value = ("-----BEGIN RSA PRIVATE KEY-----`n" + [System.Convert]::ToBase64String($clientprivateKeyFromCert.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob), [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END RSA PRIVATE KEY-----");
        Encoding = "Ascii";
    }
    Set-Content  @splat

If everything worked correctly you are supposed to see 3 certificates on your machine listed below, which are CA certificate, server certificate and client certificate, you also shall have private key for each of those (indicated by key image).

mmc_2017-06-10_16-10-12

You shall also have 5 files created in c:\test folder like below

&amp;amp;amp;amp;amp;amp;nbsp;Directory: C:\test

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----        6/10/2017   4:09 PM           3314 clientPrivateKey.cer
-a----        6/10/2017   4:09 PM           1858 clientPublicKey.cer
-a----        6/10/2017   4:09 PM           3310 privateKey.cer
-a----        6/10/2017   4:09 PM           1812 rootCA.cer
-a----        6/10/2017   4:09 PM           1936 serverCert.cer

Add your CA root certificate to your trusted root certificate authorities in certmgr.msc. Make sure you copy and paste and not move CA root certificate since if you move you will not be able to sign any more keys. This is not not a requirement but will allow you to use native Windows tools in working with certificates instead of relying on file based store like openSSL does. For example you will be able to use HTTPS to call docker REST API both in browser and via Invoke-WebRequest

Deploy server certificate to docker container host

  • Open file named daemon.json under  $env:programdata\docker\config and paste lines in snippet below into it.
    "tlscacert":  "C:\\ProgramData\\docker\\certs.d\\rootCA.cer",
    "tlskey":  "C:\\ProgramData\\docker\\certs.d\\privateKey.cer",
    "hosts":  [
                  "tcp://0.0.0.0:2376",
                  "npipe://"
              ],
    "tlscert":  "C:\\ProgramData\\docker\\certs.d\\serverCert.cer",
    "tlsverify":  true
  • Copy files rootCA.cer, privateKey.cer, serverCert.cer to $env:programdata\docker\certs.d
  • Restart docker service Restart-Service docker

At this point you shall not be able to connect to daemon via HTTPS without providing a valid certificate. Try following  Invoke-WebRequest https://containerhost1:2376, it shall fail complaining that SSL client certificate is required for connection

The request was aborted: Could not create SSL/TLS secure channel.
At line:1 char:1
+ Invoke-WebRequest https://containerhost1:2376
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

Attach client certificate to request by first finding thumbprint of your client cert in certificates manager and then use tab completion to iterate to it in get-item cert:\CurrentUser\My\, assign it to variable which you will attach to REST request $cert = get-item Cert:\CurrentUser\My\350A62B64152D9B85673E902A1F1C2CB6766598E

Issue the same request as above and it will succeed returning information about available images on remote system

PS >(Invoke-WebRequest https://containerhost1:2376/images/json -Certificate $cert -UseBasicParsing).Content | convertfrom-json

Containers : -1
Created : 1494389426
Id : sha256:242b8694ed621610a27746e0075c95e87f1a239e1800a4ea55e753010a49d9d5
Labels :
ParentId :
RepoDigests : {stefanscherer/dockertls-windows@sha256:5fe358a57cb31f18d2d148b0481898d530a5547c4d5d6f9ce5e0334ed8d3de19}
RepoTags : {stefanscherer/dockertls-windows:latest}
SharedSize : -1
Size : 1049291645
VirtualSize : 1049291645

One last thing is to try to use docker CLI to query the same information.

PS C:\admin> docker --tlsverify --tlscacert=c:\test\rootca.cer --tlscert=c:\test\clientPublicKey.cer --tlskey=c:\test\clientPrivateKey.c
er -H=tcp://containerhost1:2376 images

time="2017-06-10T16:46:14-05:00" level=info msg="Unable to use system certificate pool: crypto/x509: system root pool is not available on Windows"
REPOSITORY                        TAG                 IMAGE ID            CREATED             SIZE
stefanscherer/dockertls-windows   latest              242b8694ed62        4 weeks ago         1.05 GB

Full script is below

$ErrorActionPreference = "Stop"
if ([int](Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"  -Name Release).Release -lt 393295) {
    throw "Your version of .NET framework is not supported for this script, needs at least 4.6+"
}
function GenerateCerts {
    $splat = @{
        type = "Custom" ;
        KeyExportPolicy = "Exportable";
        Subject = "CN=Docker TLS Root";
        CertStoreLocation = "Cert:\CurrentUser\My";
        HashAlgorithm = "sha256";
        KeyLength = 4096;
        KeyUsage = @("CertSign", "CRLSign");
        TextExtension = @("2.5.29.19 ={critical} {text}ca=1")
    }
    $rootCert = New-SelfSignedCertificate @splat
    $splat = @{
        Path = "c:\test\rootCA.cer";
        Value = "-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($rootCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----";
        Encoding = "ASCII";
    }
    Set-Content @splat
    $splat = @{
        CertStoreLocation = "Cert:\CurrentUser\My";
        DnsName = "swarmmanager1", "localhost", "containerhost1";
        Signer = $rootCert ;
        KeyExportPolicy = "Exportable";
        Provider = "Microsoft Enhanced Cryptographic Provider v1.0";
        Type = "SSLServerAuthentication";
        HashAlgorithm = "sha256";
        TextExtension = @("2.5.29.37= {text}1.3.6.1.5.5.7.3.1");
        KeyLength = 4096;
    }
    $serverCert = New-SelfSignedCertificate @splat
    $splat = @{
        Path = "c:\test\serverCert.cer";
        Value = "-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($serverCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----";
        Encoding = "Ascii"
    }
    Set-Content @splat

    $privateKeyFromCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($serverCert)
    $splat = @{
        Path = "c:\test\privateKey.cer";
        Value = ("-----BEGIN RSA PRIVATE KEY-----`n" + [System.Convert]::ToBase64String($privateKeyFromCert.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob), [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END RSA PRIVATE KEY-----");
        Encoding = "Ascii";
    }
    Set-Content @splat

    $splat = @{
        CertStoreLocation = "Cert:\CurrentUser\My";
        Subject = "CN=clientCert";
        Signer = $rootCert ;
        KeyExportPolicy = "Exportable";
        Provider = "Microsoft Enhanced Cryptographic Provider v1.0";
        TextExtension = @("2.5.29.37= {text}1.3.6.1.5.5.7.3.2") ;
        HashAlgorithm = "sha256";
        KeyLength = 4096;
    }
    $clientCert = New-SelfSignedCertificate  @splat
    $splat = @{
        Path = "c:\test\clientPublicKey.cer" ;
        Value = ("-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($clientCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----");
        Encoding = "Ascii";
    }
    Set-Content  @splat
    $clientprivateKeyFromCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($clientCert)
    $splat = @{
        Path = "c:\test\clientPrivateKey.cer";
        Value = ("-----BEGIN RSA PRIVATE KEY-----`n" + [System.Convert]::ToBase64String($clientprivateKeyFromCert.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob), [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END RSA PRIVATE KEY-----");
        Encoding = "Ascii";
    }
    Set-Content  @splat
}
GenerateCerts

& docker --tlsverify --tlscacert=c:\test\rootca.cer --tlscert=c:\test\clientPublicKey.cer --tlskey=c:\test\clientPrivateKey.cer -H=tcp://containerhost1:2376 images