How To Manage Your KVM Virtual Machines Using Docker and Webvirtmgr

21 June 2016

In this article we will learn about a new concept, the Dockerfile. The Dockerfile is a special file that contains instructions that are used by Docker to create a container image. To better understand how the process works, I will explain how to configure and create a container image for webvirtmgr, a web based virtual machine manager.

The first step is to setup the build environment, which is creating a directory where the files needed will be placed; once this is done, we can create the Dockerfile and start to edit it:

purple@docker:~$ mkdir webvirtmgr/
purple@docker:~$ cd webvirtmgr/
purple@docker:~/webvirtmgr$ nano Dockerfile

Dockerfiles are simple text files that, as I mentioned before, contain instructions to build the image, these instructions start with a keyword and are followed by arguments. A mandatory keyword in the beginning of the file is the FROM keyword, which specifies where Docker should start the build. This start point is usually a base image; in our case we will use Alpine Linux:

FROM alpine:3.3

Because this image is a base image, ours is about 5MB, it contains a very small set of packages, so we will need to add some packages in it using the RUN keyword:

RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >>/etc/apk/repositories

RUN apk add --no-cache nano git nginx supervisor openssh-client cyrus-sasl cyrus-sasl-digestmd5 py-pip py-libvirt py-libxml2 py-websockify@testing \
&& rm -rf /var/cache/apk/*

The RUN keyword can be used to execute commands during the image creation process, basically it will execute the commands in the future container, but the effects of these commands will be permanent:

RUN mkdir /srv && cd /srv && git clone https://github.com/retspen/webvirtmgr

RUN cd /srv/webvirtmgr && pip install -r requirements.txt 

RUN cd /srv/webvirtmgr && ./manage.py syncdb --noinput && ./manage.py collectstatic --noinput \
&& echo "from django.contrib.auth.models import User; User.objects.create_superuser('root', 'admin@purplesrl.com', 'root')" | python manage.py shell

Because the configurations in the container are standard and we need custom configuration files we will use the ADD keyword to add some files inside the new image which need to be located in the files folder:

ADD files/nginx.conf /etc/nginx/nginx.conf
ADD files/supervisor.conf /etc/supervisor.d/webvirtmgr.ini

RUN chmod 600 /srv/webvirtmgr/.ssh/*
RUN chown -R nginx.nginx /srv/webvirtmgr && sed -i 's/var\/www\/localhost\/htdocs/srv\/webvirtmgr/' /etc/passwd

The modifications on the image are complete, but we need it to talk to the outside world, so we will use EXPOSE to make the ports 80 and 6080 available:


Finally, we need to add an ENTRYPOINT for our image, this command will be executed when the container is started, in our case it will be supervisor:

ENTRYPOINT ["/usr/bin/supervisord"]

It is worth mentioning that the CMD keyword can be used along with ENTRYPOINT to specify arguments. Docker has a default entrypoint /bin/sh -c, but has no default command. In our case we have changed the default entrypoint to /usr/bin/supervisord, therefore if we want to pass some arguments to it, we need to add them using a CMD keyword:

CMD ["--nodaemon"]

Then we will need to add our configuration files in the files directory:

# supervisor.conf


command=/usr/bin/python /srv/webvirtmgr/manage.py run_gunicorn -c /srv/webvirtmgr/conf/gunicorn.conf.py

command=/usr/bin/python /srv/webvirtmgr/console/webvirtmgr-console
# nginx.conf
daemon off;
worker_processes  1;

events {
    worker_connections  1024;

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

server {
    listen 80 default_server;

    server_name $hostname;
    access_log /var/log/nginx/webvirtmgr_access_log;

    location /static/ {
        root /srv/webvirtmgr/webvirtmgr;
        expires max;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
        proxy_set_header Host $host:$server_port;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 600;
        proxy_read_timeout 600;
        proxy_send_timeout 600;
        client_max_body_size 1024M; # Set higher depending on your needs

# ssh_config

and generate our private SSH key pair without adding any passphrase. The private key will be installed in the image, while the public key needs to be placed on the hypervisor.

purple@docker:~/webvirtmgr/files$ ssh-keygen -t rsa -b 4096 
Generating public/private rsa key pair.
Enter file in which to save the key (/home/purple/.ssh/id_rsa): ./id_rsa
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in ./id_rsa.
Your public key has been saved in ./id_rsa.pub.
The key fingerprint is:
04:3d:89:55:9d:a5:02:8f:4e:80:ce:d3:4a:57:5d:98 purple@docker

Before we are ready to build our new image using the build command, we need to make sure the file is named Dockerfile, or else it will not be found and the build process will fail:

purple@docker:~/webvirtmgr$ docker build -t webvirtmgr . 
Sending build context to Docker daemon 65.54 kB
Step 1 : FROM alpine:3.3
 ---> 184352182c50
Step 2 : RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >>/etc/apk/repositories
Step 15 : ENTRYPOINT /usr/bin/supervisord2
 ---> Running in 4f163ddc67d2
 ---> 84d89f4583d7
Removing intermediate container 4f163ddc67d2
Successfully built 84d89f4583d7

If everything worked you should see a new Docker image in the repository:

purple@docker:~/webvirtmgr$ docker images | head -n 2
webvirtmgr    latest  84d89f4583d7   56 seconds ago      157.1 MB

Some times we are not sure how other people created their images because we do not have the Dockerfile; this information can be easily obtained using the history command:

purple@docker:~/webvirtmgr$ docker history webvirtmgr
IMAGE CREATED      CREATED BY                                      SIZE
84d48 16 hours ago /bin/sh -c #(nop) ENTRYPOINT ["/usr/bin/super   0
6a7b1 2 days ago   /bin/sh -c #(nop) EXPOSE 6080/tcp               0
5b2d4 2 days ago   /bin/sh -c #(nop) EXPOSE 80/tcp                 0  
e6078 2 days ago   /bin/sh -c chown -R nginx.nginx /srv/webvirtm   7.288 MB
ac525 2 days ago   /bin/sh -c echo "@testing http://nl.alpinelin   154 B
18435 2 weeks ago  /bin/sh -c #(nop) ADD file:f6b806676d872f26a4   4.793 MB

The history command will show what modifications were made to the image, at what time and what was the size impact of the command on the image, how much was added to it. The interesting fact is that here we can see the same list of instructions that are present in our Dockerfile.

Finally one should know that these are not the only keywords that can be used in a Dockerfile, for a complete list and more information you can go on the Docker website.


by George Lucian Tabacar

Want to learn more?