How to use containers in data science with Docker and Azure: Part 1

An illustration depicting containers, with a picture of Bit the Raccoon on the right.

In the first part of this introduction to containerisation, Jon Machtynger, Cloud Solution Architect for Advanced Analytics and AI at Microsoft, reveals what it can do for data scientists

Data science is a broad church, but there are common themes, and most practitioners have a general interest in how to operationalise data science processes. With this in mind, we’ve created a no-nonsense guide to getting with containerisation, which is increasingly being used by data scientists looking to standardise projects, and port easily from a local to a cloud environment.

In this containerisation primer I’ll be using Docker, an application that enables you to build, test and ship containerised applications from your PC or Mac. Other apps are available, but Docker is accessible, easy to install and is currently used by millions of developers.

Docker scales and provides resilience using orchestration capabilities such as Kubernetes. And it’s also relatively easy to move to the cloud, where it both scales and provides a mechanism for collaborating across many more people in a consistent fashion.

In this first instalment you will learn how to:

  • Use a simple base image, build on it, and interact with it securely.
  • Upload this to Azure and then remotely execute the same functionality there.
  • Show how others might deploy that same functionality in their local environment.

We won’t cover the basics of how to install Docker, as you can find details on how to do this online, either via the official Docker support site, or elsewhere. I also assume that you already know the basics of what containers are, and how they differ from virtual machines (VMs).

There are many articles, such as About Docker, What is a Container, Installing Docker on Linux, Installing Docker on Windows, and Dockerfile Reference, that show how to install Docker, download containers or build a container.

But with this series, we will approach using Docker from a data science angle. And I’ll focus on the following data science inhibitors:

  • Minimising conflicting library versions for different development projects.
  • Consistency across environments and developers against specific design criteria.
  • Avoiding a need to reinstall everything on new hardware after a refresh or failure.
  • Maximise collaboration clarity across groups. Consistent libraries, result sets, etc.
  • Extending on-premises autonomy/agility to cloud scale and reach.

We’ll interact with a container as though it was a separate, self-contained process you have complete access to, because that is – after all – what it is. This is a key concept to appreciate.

Getting started

Let’s start with some assumptions about working with containers:

  • Containers are expected to be stateless and disposable. They provide compute and when they exit, everything inside that container could disappear with it. Because they’re stateless, they also provide a basis for scalability and resilience.
  • A container should focus on as few things as possible. This supports a micro-services approach and allows designers to combine primitive containers in interesting combinations without overly worrying about interdependencies.

Building a simple Container

For the examples in this article, you should create a working directory.  In that directory, we’re going to build a new container based on a very small image called alpine.

$ mkdir -p docker4ds
$ cd docker4ds
$ touch Dockerfile

Now edit that Dockerfile to hold the following:

FROM alpine

RUN apk --update add --no-cache openssh
RUN echo 'root:rootpwd' | chpasswd

# Modify sshd_config items to allow login
RUN sed -i 's/#PermitRootLogin.*/PermitRootLogin\ yes/' /etc/ssh/sshd_config && \
    sed -ie 's/#Port 22/Port 22/g' /etc/ssh/sshd_config && \
    sed -ri 's/#HostKey \/etc\/ssh\/ssh_host_key/HostKey \/etc\/ssh\/ssh_host_key/g' /etc/ssh/sshd_config && \
    sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_rsa_key/HostKey \/etc\/ssh\/ssh_host_rsa_key/g' /etc/ssh/sshd_config && \
    sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_dsa_key/HostKey \/etc\/ssh\/ssh_host_dsa_key/g' /etc/ssh/sshd_config && \
    sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_ecdsa_key/HostKey \/etc\/ssh\/ssh_host_ecdsa_key/g' /etc/ssh/sshd_config && \
    sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_ed25519_key/HostKey \/etc\/ssh\/ssh_host_ed25519_key/g' /etc/ssh/sshd_config

# Generate new keys
RUN /usr/bin/ssh-keygen -A && ssh-keygen -t rsa -b 4096 -f  /etc/ssh/ssh_host_key

CMD ["/usr/sbin/sshd","-D"]  # Start the ssh daemon

This starts FROM a base Linux image called alpine, and then adds some custom functionality. I’m going to create a small SSH server, but in practice, I could add anything. The base image is pulled from the public Docker registry, but later I’ll show you how you can also pull your content from a private registry in Azure.

With the first RUN, I add the openssh package to the image and then assign a new password to root. Doing this in clear text isn’t secure practice, but I’m only showing the flexibility of a Dockerfile.

A Dockerfile can have many RUN steps, each of which, is an image build step that provides a layer of committed change to the eventual docker image. I then modify the sshd_config file to allow remote login and generate new SSH keys.

Lastly, I start the SSH daemon using CMD (CMD is the command that the container executes by default when you launch the build image, and a Dockerfile can only have one CMD).

You can find some Dockerfile best practices here.

Now let’s build the image:

$ docker build -t jon/alpine:1.0 .
Sending build context to Docker daemon  109.1kB
Step 1/6 : FROM alpine
latest: Pulling from library/alpine
bdf0201b3a05: Pull complete
Digest: sha256:28ef97b8686a0b5399129e9b763d5b7e5ff03576aa5580d6f4182a49c5fe1913
Status: Downloaded newer image for alpine:latest
 ---> cdf98d1859c1
Step 2/6 : RUN apk --update add --no-cache openssh
 ---> Running in d9aa96d42532
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/community/x86_64/APKINDEX.tar.gz
(1/10) Installing openssh-keygen (7.9_p1-r4)
(2/10) Installing ncurses-terminfo-base (6.1_p20190105-r0)
(3/10) Installing ncurses-terminfo (6.1_p20190105-r0)
(4/10) Installing ncurses-libs (6.1_p20190105-r0)
(5/10) Installing libedit (20181209.3.1-r0)
(6/10) Installing openssh-client (7.9_p1-r4)
(7/10) Installing openssh-sftp-server (7.9_p1-r4)
(8/10) Installing openssh-server-common (7.9_p1-r4)
(9/10) Installing openssh-server (7.9_p1-r4)
(10/10) Installing openssh (7.9_p1-r4)
Executing busybox-1.29.3-r10.trigger
OK: 17 MiB in 24 packages
Removing intermediate container d9aa96d42532
 ---> 1acd76f36c6b
Step 3/6 : RUN echo 'root:rootpwd' | chpasswd
 ---> Running in 8e4a2d38bd60
chpasswd: password for 'root' changed
Removing intermediate container 8e4a2d38bd60
 ---> 4e26a17c921e
Step 4/6 : RUN 	sed -i 's/#PermitRootLogin.*/PermitRootLogin\ yes/' /etc/ssh/sshd_config && 	sed -ie 's/#Port 22/Port 22/g' /etc/ssh/sshd_config && 	sed -ri 's/#HostKey \/etc\/ssh\/ssh_host_key/HostKey \/etc\/ssh\/ssh_host_key/g' /etc/ssh/sshd_config && 	sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_rsa_key/HostKey \/etc\/ssh\/ssh_host_rsa_key/g' /etc/ssh/sshd_config && 	sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_dsa_key/HostKey \/etc\/ssh\/ssh_host_dsa_key/g' /etc/ssh/sshd_config && 	sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_ecdsa_key/HostKey \/etc\/ssh\/ssh_host_ecdsa_key/g' /etc/ssh/sshd_config && 	sed -ir 's/#HostKey \/etc\/ssh\/ssh_host_ed25519_key/HostKey \/etc\/ssh\/ssh_host_ed25519_key/g' /etc/ssh/sshd_config
 ---> Running in 3c85a906e8cd
Removing intermediate container 3c85a906e8cd
 ---> 116defd2d657
Step 5/6 : RUN 	/usr/bin/ssh-keygen -A && ssh-keygen -t rsa -b 4096 -f  /etc/ssh/ssh_host_key
 ---> Running in dba2ff14a17c
ssh-keygen: generating new host keys: RSA DSA ECDSA ED25519
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /etc/ssh/ssh_host_key.
Your public key has been saved in /etc/ssh/ssh_host_key.pub.
The key fingerprint is:
SHA256:T4/Z8FLKLdEwZsTTIhx/g0A5DDuejhcl7B9FdsjFyCc root@dba2ff14a17c
The key's randomart image is:
+---[RSA 4096]----+
|      .=+*+*o    |
|     . .**Eo+    |
|      = .oO=o    |
|     o = + = .   |
|      = S + o    |
|     o o = %     |
|    . o . O =    |
|     .     o     |
|                 |
+----[SHA256]-----+
Removing intermediate container dba2ff14a17c
 ---> 49ee4b262ae4
Step 6/6 : CMD ["/usr/sbin/sshd","-D"]  # Start the ssh daemon
 ---> Running in 2a074ec11e30
Removing intermediate container 2a074ec11e30
 ---> cf85e38faa5e
Successfully built cf85e38faa5e
Successfully tagged jon/alpine:1.0
$

If I now look at my available images, I have the core alpine image used as a basis for my image, and my custom image, which includes the SSH service. Look how tiny my image is – a working SSH server in under 13MB:

$ docker images
REPOSITORY             TAG            IMAGE ID            CREATED             SIZE
jon/alpine             1.0            cf85e38faa5e        52 seconds ago      12.5MB
alpine                 latest         cdf98d1859c1        8 days ago          5.53MB

Notice that during the build of jon/alpine:1.0, the base alpine image had an ID of cdf98d1859c1, which also shows up in the Docker image lists. Let’s use it.

The following creates, and then runs a container based on the jon/alpine:1.0 image. It also maps port 2222 on my local machine to port 22 within the running container, which is the default SSH login port. When I create that container, it returns a unique identifier, and if I list running containers, it shows a unique container ID that prefixed that text:

$ docker run -d -p 2222:22 jon/alpine:1.0 
db50da6f71ddeb69f1f3bdecc4b0a01c48fcda93f68ee21f2c14032e995d49ff 
$
$ docker ps -a
CONTAINER ID  IMAGE            COMMAND               CREATED         STATUS        PORTS                 NAMES
db50da6f71dd  jon/alpine:1.0   "/usr/sbin/sshd -D"   5 minutes ago   Up 5 minutes  0.0.0.0:2222->22/tcp  obj_austin

I should now also be able to SSH into that container. I log in through port 2222, which maps to the container’s SSH port 22. The hostname for that container is the container ID:

$ ssh root@localhost -p 2222
The authenticity of host '[localhost]:2222 ([::1]:2222)' can't be established.
ECDSA key fingerprint is SHA256:IxFIJ25detXF9HTc5CHffkO2DmhBzBe6EFRqFVj5H6w.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[localhost]:2222' (ECDSA) to the list of known hosts.
root@localhost's password:
Welcome to Alpine!

The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See .

You can setup the system with the command: setup-alpine

You may change this message by editing /etc/motd.

db50da6f71dd:~#

Multiple containers often work together owning different capabilities or scaling compute power by running in parallel. Let’s start more of these, each with a different mapped port:

$ docker run -d -p 2223:22 jon/alpine:1.0
5821d6a9e8c73ae0d64f7c59199a948cd43e87d6019b05ff54f01df83557b0f3
$ docker run -d -p 2224:22 jon/alpine:1.0
0305ea0aaf5142c6a89f8802fb67c3b1a768094a81be2bf15578b933c3385f87
$ docker run -d -p 2225:22 jon/alpine:1.0
1e0f3f2ac16f5fcd9a1bb169f07930061d42daea6aec8afeb08132ee5dd5c896
$
$ docker ps -a
CONTAINER ID    IMAGE            COMMAND              CREATED          STATUS          PORTS                  NAMES
1e0f3f2ac16f    jon/alpine:1.0   "/usr/sbin/sshd -D"  34 seconds ago   Up 33 seconds   0.0.0.0:2225->22/tcp   loving_kare
0305ea0aaf51    jon/alpine:1.0   "/usr/sbin/sshd -D"  39 seconds ago   Up 38 seconds   0.0.0.0:2224->22/tcp   nab_ritchie
5821d6a9e8c7    jon/alpine:1.0   "/usr/sbin/sshd -D"  44 seconds ago   Up 42 seconds   0.0.0.0:2223->22/tcp   det_feynman
db50da6f71dd    jon/alpine:1.0   "/usr/sbin/sshd -D"  12 minutes ago   Up 12 minutes   0.0.0.0:2222->22/tcp   obj_austin

After each container starts, it tells me its container ID. You can actually use any of a container’s initial uniquely identifying characters to work with it.

Let’s find out more about container 1e0f3f2ac16f using docker inspect 1e0. I’m interested in some network details. I only use the first three characters because they’re enough to uniquely identify it. In fact, I could have identified it with just ‘1’ as no other container ID started with that:

$ docker inspect 1e0 | grep -i address
            "LinkLocalIPv6Address": "",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "GlobalIPv6Address": "",
            "IPAddress": "172.17.0.5",
            "MacAddress": "02:42:ac:11:00:05",
                    "IPAddress": "172.17.0.5",
                    "GlobalIPv6Address": "",
                    "MacAddress": "02:42:ac:11:00:05",
$

Now let’s interact with it directly:

$ docker exec -it 1e0 ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:05
          inet addr:172.17.0.5  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:21 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1558 (1.5 KiB)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
$

Here, I executed the ipconfig command interactively with the container identified (uniquely) by the identifier 1e0. Note that both approaches provided the same network and MAC address. Once the command finishes, control returns to your host. Let’s interact with another container and look at its network details using both approaches:

$ docker exec -it 030 ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:04
          inet addr:172.17.0.4  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:21 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1558 (1.5 KiB)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

$ docker inspect 030 | grep -i address
            "LinkLocalIPv6Address": "",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "GlobalIPv6Address": "",
            "IPAddress": "172.17.0.4",
            "MacAddress": "02:42:ac:11:00:04",
                    "IPAddress": "172.17.0.4",
                    "GlobalIPv6Address": "",
                    "MacAddress": "02:42:ac:11:00:04",$

This shows that the second container is running, and shares a common Docker network.

Now let’s move this capability to the cloud. If you don’t have an Azure account, you can get a free trial here.  You’ll also need to install the Azure CLI client, which you can get here.

Now let’s create a resource group to hold (and ringfence) all our resources. This will allow us to cleanly remove it all once we’re done playing with it.

I’m going to use the resource group docker-rg, so do the same, or choose one that works for you. I’m choosing ‘East US’ as my location. Feel free to choose one close to you, but for the purposes of this tutorial, it should make no difference:

$ az group create --name docker-rg --location eastus
{
  "id": "/subscriptions//resourceGroups/docker-rg",
  "location": "eastus",
  "managedBy": null,
  "name": "docker-rg",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": null,
  "type": null
}
$

Within your resource group, you can now create an Azure container registry with the az acr create command. The container registry name must be unique within Azure. I’m using joncreg as my registry name, within my docker-rg resource group.

$ az acr create --resource-group docker-rg --name joncreg --sku Basic --admin-enabled true
{
  "adminUserEnabled": true,
  "creationDate": "2019-04-17T10:31:54.591280+00:00",
  "id": "/subscriptions//resourceGroups/docker-rg/providers/Microsoft.ContainerRegistry/registries/joncreg",
  "location": "eastus",
  "loginServer": "joncreg.azurecr.io",
  "name": "joncreg",
  "networkRuleSet": null,
  "provisioningState": "Succeeded",
  "resourceGroup": "docker-rg",
  "sku": {
    "name": "Basic",
    "tier": "Basic"
  },
  "status": null,
  "storageAccount": null,
  "tags": {},
  "type": "Microsoft.ContainerRegistry/registries"
}
$

This provides you with quite a bit of information, but if you log in to your resource group, you can also find the name of your registry login server. We’ll use this later as we start storing and retrieving content from that repository:

$ az acr login --name joncreg
Login Succeeded
$
$ az acr show --name joncreg --query loginServer --output table
Result
------------------
joncreg.azurecr.io
$

Now let’s tag your local machine against that Azure login server. This will allow us to refer to and manage it as a cloud resource. Notice that the Image IDs for both your local image and the Azure image are currently identical:

$ docker tag jon/alpine:1.0 joncreg.azurecr.io/alpine:1.0
$ docker images
REPOSITORY                            TAG              IMAGE ID            CREATED             SIZE
joncreg.azurecr.io/alpine             1.0              cf85e38faa5e        10 hours ago        12.5MB
jon/alpine                            1.0              cf85e38faa5e        10 hours ago        12.5MB
$

And now let’s push the tagged image out to the Azure Container Registry, and then confirm that the image is there:

$ docker push joncreg.azurecr.io/alpine:1.0
The push refers to repository [joncreg.azurecr.io/alpine]
06c6815029d6: Pushed
0b334f069f0f: Pushed
438f073b5999: Pushed
9d8531f069a1: Pushed
a464c54f93a9: Pushed
1.0: digest: sha256:d5080107847050caa2b21124142c217c10b38c776b3ce3a6611acc4116dcabb0 size: 1362
$
$ az acr repository list --name joncreg --output table
Result
--------
alpine
$
$ az acr repository show-tags --name joncreg --repository alpine --output table
Result
--------
1.0
$

So, what can we do with this container in the cloud?

We can execute it, share it with others, or use it as a base for building other containers. But in order to access it, you’ll need to do it securely. Let’s create a service principal, which we’ll use to later. In this script, I add my container registry name (joncreg) and specify a unique service principal name (jon-acr-sp):

#!/bin/bash

# Modify for your environment.
# ACR_NAME: The name of your Azure Container Registry
# SERVICE_PRINCIPAL_NAME: Must be unique within your AD tenant
ACR_NAME="joncreg"
SERVICE_PRINCIPAL_NAME=jon-acr-sp

# Obtain the full registry ID for subsequent command args
ACR_REGISTRY_ID=$(az acr show --name $ACR_NAME --query id --output tsv)

# Create the service principal with rights scoped to the registry.
# Default permissions are for docker pull access. Modify the '--role'
# argument value as desired:
# acrpull:     pull only
# acrpush:     push and pull
# owner:       push, pull, and assign roles
SP_PASSWD=$(az ad sp create-for-rbac --name http://$SERVICE_PRINCIPAL_NAME --scopes $ACR_REGISTRY_ID --role acrpull --query password --output tsv)
SP_APP_ID=$(az ad sp show --id http://$SERVICE_PRINCIPAL_NAME --query appId --output tsv)

# Output the service principal's credentials; use these in your services and
# applications to authenticate to the container registry.
echo "Service principal ID: $SP_APP_ID"
echo "Service principal password: $SP_PASSWD"

When I run this, I get returned a secure principle ID and a password. I’ve obscured mine for obvious reasons, but you will need to remember these for later, so make a copy:

Service principal ID: 6xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx2
Service principal password: 6xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxf

Now let’s create and deploy a container, which I’ll call it alpine-ssh. It doesn’t do much, so I’ll only allocate a single virtual CPU and 100MB of RAM to it:

$ az container create --resource-group docker-rg --name alpine-ssh --image joncreg.azurecr.io/alpine:1.0 --cpu 1 --memory 0.1 --registry-login-server joncreg.azurecr.io --registry-username 6xxxxxxx-xxxx-xxxx-xxxb-xxxxxxxxxxx2 --registry-password 6xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxf --dns-name-label jm-alpine-ssh-314159 --ports 22
{
  .
 
  .
}
$

You should see a JSON response with loads of details confirming your deployment, but you can also find the fully qualified DNS name for your new container using the following:

$ az container show --resource-group docker-rg --name alpine-ssh --query ipAddress.fqdn
"jm-alpine-ssh-314159.eastus.azurecontainer.io"
$

It’s deployed. And I can now SSH to that address to log in:

$ ssh root@jm-alpine-ssh-314159.eastus.azurecontainer.io
The authenticity of host 'jm-alpine-ssh-314159.eastus.azurecontainer.io (20.185.98.127)' can't be established.
ECDSA key fingerprint is SHA256:IxFIJ25detXF9HTc5CHffkO2DmhBzBe6EFRqFVj5H6w.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'jm-alpine-ssh-314159.eastus.azurecontainer.io,20.185.98.127' (ECDSA) to the list of known hosts.
root@jm-alpine-ssh-314159.eastus.azurecontainer.io's password:
Welcome to Alpine!

The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See <http://wiki.alpinelinux.org/>.

You can setup the system with the command: setup-alpine

You may change this message by editing /etc/motd.

wk-caas-edf9b7736da8406395657de1be9212b0-f3c593a7045e59fd38bbf0:~#

Simple, right? But I’d like to make it easy for other people to use this image as well. Stop and remove all your running container images:

$ docker ps -q
1e0f3f2ac16f
0305ea0aaf51
5821d6a9e8c7
db50da6f71dd
$
$ RUNNING=$(docker ps -q)
$ docker stop $RUNNING
1e0f3f2ac16f
0305ea0aaf51
5821d6a9e8c7
db50da6f71dd
$ docker rm $RUNNING
1e0f3f2ac16f
0305ea0aaf51
5821d6a9e8c7
db50da6f71dd
$

Lastly, remove the images holding your original SSH server:

$ docker images
REPOSITORY                            TAG              IMAGE ID            CREATED             SIZE
joncreg.azurecr.io/alpine             1.0              cf85e38faa5e        10 hours ago        12.5MB
jon/alpine                            1.0              cf85e38faa5e        10 hours ago        12.5MB
$
$ docker rmi jon/alpine:1.0
$ docker rmi joncreg.azurecr.io/alpine:1.0

There is now no local copy of your Docker image, but if you still need to run it locally, then this isn’t a problem. You shouldn’t have to rebuild it. You – and others you allow – can simply get it directly from Azure. So, let’s run one – it will see that there isn’t one locally, download it, and then run the container locally:

$ docker run -d -p 2222:22 joncreg.azurecr.io/alpine:1.0
Unable to find image 'joncreg.azurecr.io/alpine:1.0' locally
1.0: Pulling from alpine
bdf0201b3a05: Already exists
9cb9180e5bb6: Pull complete
6425579f73e9: Pull complete
b7eda421926c: Pull complete
163c36e4f93a: Pull complete
Digest: sha256:d5080107847050caa2b21124142c217c10b38c776b3ce3a6611acc4116dcabb0
Status: Downloaded newer image for joncreg.azurecr.io/alpine:1.0
65fef6f65c6c9fc766e13766d7db4316825170e7ff7923db824127ed78ad0970

$ docker ps
CONTAINER ID        IMAGE                           COMMAND               CREATED              STATUS              PORTS                  NAMES
65fef6f65c6c        joncreg.azurecr.io/alpine:1.0   "/usr/sbin/sshd -D"   About a minute ago   Up About a minute   0.0.0.0:2222->22/tcp   gracious_khorana

We’ve pulled that image from your Azure container registry. If you get an error saying that you need to log in to your Azure Docker container environment, use the following (with your service principal username and password) and retry the previous docker run command:

$ az acr login --name joncreg --username 6xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx2 --pass 6xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxf
Login Succeeded
$

Note: Once you’ve finished, and you’ve created a specific resource group to test this, you might want to consider removing the resource group. It will also stop you being charged for things you’re no longer going to use. In my case, I did the following:

$ az group delete --name docker-rg

Conclusion

This brings the first phase of our project to an end. Using a standards-based cross platform environment (Docker), you created a virtual compute (Alpine Linux) resource, which was then moved to the cloud (we used Alpine, but you could do the same with other environments such as Ubuntu, CentOS, Windows, etc.).

We then made the image available to be shared as a standard environment, with the option to maintain multiple versions of it (e.g. 1.0, 1.1, 2.0 etc.). Assuming a user has permission, any Docker environment can pull and run that standard image. This means you can package a very specific combination of libraries, environments, and tools, to develop, test, and run in a consistent fashion.

Next time

In the next part of this series, we’ll look at data persistence using containers and how this can be provided locally and in the cloud. This starts to provide a foundation for something more usable from a data science perspective such as shared code, database access, and shared development frameworks.

Find out more

[msce_cta layout=”image_center” align=”center” linktype=”blue” imageurl=”https://www.microsoft.com/en-gb/industry/blog/wp-content/uploads/sites/22/2019/05/CLO19_azureKinectDK_024.jpg” linkurl=”https://azure.microsoft.com/en-gb/product-categories/containers/” linkscreenreadertext=”Accelerate your apps with containers” linktext=”Accelerate your apps with containers” imageid=”11460″ ][/msce_cta]

About the author

Jon MachtyngerJon is a Microsoft Cloud Solution Architect specialising in Advanced Analytics & Artificial Intelligence.

With over 30 years of experience in understanding, translating and delivering leading technology to the market. He currently focuses on a small number of global accounts helping align AI and Machine Learning capabilities with strategic initiatives. He moved to Microsoft from IBM where he was Cloud & Cognitive Technical Leader and an Executive IT Specialist.

Jon has been the Royal Academy of Engineering Visiting Professor for Artificial Intelligence and Cloud Innovation at Surrey University since 2016, where he lectures on various topics from machine learning, and design thinking to architectural thinking.