When you first set up Containers on Windows Server 2016, you would imagine there would be some kind of management console. But there is none. You have to work entirely from the command line. Portainer provides a management GUI that makes it easier to visualise what is going on.
The Windows Container feature itself only provides the base Host Compute and Host Network Services, as Hyper-V extensions. There is no management console for these. Even if you install the Hyper-V role, as well as the Containers feature, you can’t manage images and containers from the Hyper-V management console.
Images and containers are created and managed by a third party application, Docker. Docker also has no management console. It is managed from the Docker CLI, either in PowerShell or the Command Prompt.
There is a good reason for this. But, for me at least, it makes it hard to visualise what is going on. Portainer is a simple management UI for Docker. It is open source, and itself runs as a container. It works by connecting to the Docker engine on the host server, then providing a web interface to manage Docker.
Portainer Dashboard
Setting up the Portainer container will also give us a better idea of how to work with Docker. Docker has a daunting amount of documentation for the command line, and it is not easy to get to grips with it.
Configure Docker TCP Socket
The first step in setting up Portainer is to enable the Docker service to listen on a TCP socket. By default Docker only allows a named pipe connection between client and service.
Quick version:
- create a file with notepad in C:\ProgramData\docker\config
- name the file daemon.json
- add this to the file:
{"hosts": ["tcp://0.0.0.0:2375","npipe://"]}
- restart the Docker service.
The long version is: the Docker service can be configured in two different ways:
- By supplying parameters to the service executable
- By creating a configuration file, daemon.json, in C:\ProgramData\docker\config.
The parameters for configuring the Docker service executable are here: Daemon CLI Reference. To start Docker with a listening TCP socket on port 2375, use the parameter
-H tcp://0.0.0.0:2375
This needs to be configured either directly in the registry, at HKLM\SYSTEM\CurrentControlSet\Services\Docker; or with the Service Control command line:
sc config Docker binPath= ""C:\Program Files\docker\dockerd.exe\" --run-service -H tcp://0.0.0.0:2375"
The syntax is made difficult by the spaces, which require quotation with escape characters.
The easier way is to configure the Docker service with a configuration file read at startup, daemon.json. The file does not exist by default. You need to create a new text file and save it in the default location C:\ProgramData\docker\config. The daemon.json file only needs to contain the parameters you are explicitly configuring. To configure a TCP socket, add this to the file:
{ "hosts": ["tcp://0.0.0.0:2375","npipe://"] }
Other options for the configuration file for Docker in Windows are documented here: Miscellaneous Options. For example you can specify a proxy server to use when pulling images from the Docker Hub.
Just to add complexity:
- the Docker service will not start if the same parameter is set in service startup and in the configuration file
- You can change the location of the configuration file by specifying a parameter for the service:
sc config Docker binPath= ""C:\Program Files\docker\dockerd.exe\" --run-service --config-file "[path to file]""
Ports 2375 (unencrypted) and 2376 (encrypted with TLS) are the standard ports. You will obviously want to use TLS in a production environment, but the Windows Docker package does not include the tools to do this. Standard Windows certificates can’t be used. Instead you will need to follow the documentation to create OpenSSL certificates.
Allow Docker Connection Through Firewall
Configure an inbound rule in the Windows firewall to allow TCP connections to the Docker service on port 2375 or 2376. This needs to be allowed for all profiles, because the container virtual interface is detected as on a Public network.
netsh advfirewall firewall add rule name="Docker" dir=in action=allow protocol=TCP localport=2375 enable=yes profile=domain,private,public
Note that, by default, containers do not have access to services and sockets on the host.
Pull the Portainer Image
Back in an elevated PowerShell console, pull the current Portainer image from the Portainer repository in the Docker Hub:
docker pull portainer/portainer
If we look in the images folder in C:\ProgramData\docker\windowsfilter we can see that we have downloaded 6 new layers. We already had two Nano Server layers, because we pulled those down previously.
If we look at the image history, we can see the layers making up the image:
docker image history portainer/portainer
The two base layers of the Portainer image are Windows Nano Server. We already had a copy of the Nano Server base image, but ours was update 10.0.14393.1593, so we have downloaded a layer for the newer update 10.0.14393.1715. We can also see the action that created each layer.
If we inspect the image, with:
docker image inspect portainer/portainer
we can see some of the things we need to set it up
- The container is going to run portainer.exe when it starts
- The exposed port is 9000
- The volume (or folder) to mount externally is C:\Data
Set up Portainer container
Quick version:
- Create a folder in the host called: C:\ProgramData\Containers\Portainer
- Open an elevated PowerShell console on the host
- Run this command:
docker run -d --restart always --name portainer -v C:\ProgramData\Containers\Portainer:C:\Data -p 9000:9000 portainer/portainer
The long version is: we need the command line to run the Portainer image:
- Standard command to create a container:
docker run
- We want to run the container detached as a free standing container, with no attached console:
-d
or--detach
- There is no need to remove the container if it is stopped. Instead, we want to restart the container automatically if, for example, the host is rebooted:
--restart always
- We can give the container a name, to make it easier to manage:
--name portainer
- Portainer reads information about images and containers directly from Docker, so it does not need to store that. But it needs to store it’s own configuration, for example settings and user passwords. To do this, we need to save the configuration data outside the container. We can do this in Docker by mounting an external folder in the file system of the container. The folder in the container has already been designated as C:\Data in the image, but the folder in the host can be anything you choose. In this example we are using C:\ProgramData\Containers\Portainer. The folder needs to exist before using this:
-v C:\ProgramData\Containers\Portainer:C:\Data
- The Portainer process is listening on port 9000 (see above). We can connect to this directly from the host itself, without doing anything more. But the outside world has no access to it. The container is running on a virtual switch with NAT enabled. This does port forwarding from the host to the container. We need to decide what port on the host we would like to be forwarded to port 9000 on the container. If we don’t specify a port, Docker will assign a random port and we can discover it through
docker container inspect portainer
. Otherwise we can specify a port on the host, which in this case can also be 9000:-p 9000:9000
- The image to run:
portainer/portainer
- We don’t need to specify a command to run, since the image already has a default command:
portainer.exe
Putting the parameters together, the full command is:
docker run -d --restart always --name portainer -v C:\ProgramData\Containers\Portainer:C:\Data -p 9000:9000 portainer/portainer
Connect to Portainer
Using a browser on your desktop, connect to the Docker TCP port on the remote host: http://192.168.1.144:9000
. Set up a password for the admin user:
Set up the Docker host as the endpoint:
Note that the endpoint is the IP address of the host virtual interface on the container subnet (in this case 172.17.64.1). This address is also the gateway address for the container, but in this context it is not acting as a gateway. The virtual interface on the host is listening on port 2375 for Docker connections.
And we are in:
We can also connect directly from a browser on the host to the container. For this, we need to use the IP address of the container itself, in this case 172.17.68.78, or whatever address we find from docker container inspect portainer
.
The Portainer Container
We don’t need to set up a firewall rule to allow access to the container on port 9000. Docker sets up a bunch of rules automatically when the container is created:
These rules include: DHCP; ICMP; and DNS. They also include port 9000 on the host, which we specified would be forwarded to port 9000 in the container:
In Portainer, when we set up the endpoint (being Docker on the host) we need to specify the virtual interface of the host that is on the same subnet as the container (the 172.17.64.1 address). This is because Windows does not allow the container to connect directly through the virtual interface to a service on the physical interface (192.168.1.144).
If we look at the TCP connections on the host, with: netstat -a -p tcp
, we see that there is no active connection to Portainer in the container, although my browser is in fact connected from outside:
However, if we look at the NAT sessions, with Get-NetNATSession
, we see the port forwarding for port 9000 to the container:
Docker has attached a virtual hard disk to the host, being the file system of the container:
If we give it a drive letter we can see inside:
The portainer executable is in the root of the drive. C:\Data is the folder that we mounted in the docker run command. Other folders like css and fonts are part of the application. These are contained in the first layer of the image, after the Nano Server layers. The layer was created by the COPY command in the Portainer Dockerfile used to create the image:
FROM microsoft/nanoserver COPY dist / VOLUME C:\\data WORKDIR / EXPOSE 9000 ENTRYPOINT ["/portainer.exe"]
And here is the portainer process running on the host in Session 2, using:
Get-Process | Where-Object {$_.SI -eq 2} | Sort-Object SI
Security
You can see in the Portainer GUI for creating endpoints that we can connect to Docker with TLS. This assumes we have set up Docker with certificates and specified encrypted TCP connections, covered in the Docker daemon security documentation.
We should also connect to Portainer over an encrypted connection. We can do this by adding more parameters to the docker run command: Securing Portainer using SSL.
More about using Portainer
You can read more about using Portainer in the Portainer documentation.
Xuggs says:
I am not able to connect to my docker endpoint no matter what I try. Is there any chance someone can help me on this? My Portainer container itself deploys successfully but I cannot connect to it.
Air says:
Hi Xuggs, Portainer has great community support on Slack, at https://portainer.io/slack/.