Docker registry first steps
Here at Octo, we are fond of Docker. Not because we completely master it, but because we don't (yet). And as DevOps-minded guys, we like new perspectives in Dev / Ops relationship. Docker is mainly about this, shifting each other's expectation. Now that the 0.7 and 0.8 releases are out, its production readiness has never been closer and it's getting pretty exciting.
In a previous article, we've answered a few questions allowing to understand the basic concepts of Dockers, let's play around with few Docker command lines and manage a local repository (also called registry).
Reminder
Docker is a new approach in application packaging and deployment in many ways. Some quite disturbing paradigms are therefore considered:
- Ops don't have to care about how the containers are built neither do they have to care about what they are made of. They just have to consider them as application-level (almost) black boxes which are simply connected to each other. Docker containers are usually made of everything needed to run over a LXC-aware Linux kernel (libC, middleware, application). They are often based on a regular distribution (CentOS, ubuntu, Debian...).
- Container images are built once (usually by dev guys) and reused as-is everywhere, from development workstation to production envs, with no modification at all.
- Prefer the «rebuild/redeploy» pattern rather than «upgrade» when deploying new application version. Upgrades simply consist of shutting down the former container and start a new one in its place.
- Rely on registries to store and version container images.
Registries are a major component in Docker ecosystem. They are used at different stages of the application lifecycle:
- Dev guys use the docker.io public registry as some kind of github to easily get ready-to-use containers with application stacks (Java, PHP, Rails...) deployed
- Ops guys might contribute to those middleware-ready containers by applying internal security and deployent patterns, if needed.
- Dev guys uses their local Docker instance and internal Docker registries as Git with origins: (think commit, tag, pull, push) to produce ready-to-deploy applications.
- Ops guys rely on internal Docker registries as a Nexus platform: download a container based on a given version number and deploy it. Container upgrades however only download the missing commits, not all the new container.
Some examples by practice
All the following commands must be run on a LXC-aware Linux, physical or virtual, with Docker freshly install. You can follow the Ubuntu installation procedure to perform such install, it's pretty straight forward.
Let's start a local registry. It can itself run within a Docker container of course to ease its deployment. Let's use the container proposed on docker.io's registry and expose its TCP/5000
port on the host:
$ docker run -d -p=5000:5000 stackbrew/registry:latest
Now let's create a local copy of the standard ubuntu container image:
$ docker pull ubuntu:latest [...] $
If we have a look at the local images by running:
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE stackbrew/registry latest 3321c2caa0cf 3 weeks ago 472.2 MB (virtual 3 GB) ubuntu latest 8dbd9e392a96 8 months ago 128 MB (virtual 128 MB)
We can see that 2 container images are locally known.
Let's tag the ubuntu:latest to prepare it to be pushed on the local registry. Tagging is a way to give a more understandable name to a container image, but also a way to push them on a remote registry, private or public.
$ docker tag 8dbd9e392a96 127.0.0.1:5000/ubuntu:latest $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE stackbrew/registry latest 3321c2caa0cf 3 weeks ago 472.2 MB (virtual 3 GB) ubuntu latest 8dbd9e392a96 8 months ago 128 MB (virtual 128 MB) 127.0.0.1:5000/ubuntu latest 8dbd9e392a96 8 months ago 128 MB (virtual 128 MB)
ubuntu:latest
has the same IMAGE ID as the one freshly tagged onto our new registry, looks good. Let's push it on our local regsitry, listening on the TCP/5000
port. By uploading this image, we make it available to any other Docker host around.
$ docker push 127.0.0.1:5000/ubuntu The push refers to a repository [127.0.0.1:5000/ubuntu] (len: 1) Sending image list Pushing repository 127.0.0.1:5000/ubuntu (1 tags) Pushing 8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c Pushing tags for rev [8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c] on {http://127.0.0.1:5000/v1/repositories/ubuntu/tags/latest}
Now let's start our own new docker container by creating the following Dockerfile:
FROM 127.0.0.1:5000/ubuntu:latest
# tweak ubuntu's initctl
RUN dpkg-divert --local --rename --add /sbin/initctl
RUN ln -s /bin/true /sbin/initctl
RUN apt-get install -y memcached
ADD memcached.sh /
EXPOSE 11211
CMD ["/memcached.sh"]
USER daemon
You might have noticed that we added a memcached.sh
file which content might look like:
#!/bin/bash
exec memcached
It's very trivial at this stage and could have been totally written into the CMD Dockerfile statement. Let's keep it that way for further improvements.
We can now build our brand new container image:
$ docker build -t 127.0.0.1:5000/ubuntu/memcached:latest .
Once the container image has been built, we don't need the Dockerfile
anymore, except if we want to re-build the image later on or on another docker host. The local Docker agent keep tracks of this local image. It's therefore possible to have a look at the full tree of Docker images using a docker images --tree
:
└─8dbd9e392a96 Size: 128 MB (virtual 128 MB) Tags: ubuntu:latest, 127.0.0.1:5000/ubuntu:latest └─b5f46a480005 Size: 155.8 kB (virtual 128.2 MB) └─86f4d2bdda8b Size: 16 B (virtual 128.2 MB) └─55bc3a7d11cb Size: 50.99 MB (virtual 179.2 MB) └─d60079a9c49b Size: 28 B (virtual 179.2 MB) └─34d94e18df44 Size: 178.4 MB (virtual 357.6 MB) └─43f04dd23753 Size: 178.4 MB (virtual 535.9 MB) └─4fd5c706f3a8 Size: 178.4 MB (virtual 714.3 MB) Tags: 127.0.0.1:5000/ubuntu/memcached:latest
You can see that several patches (for instance, filesystem content changes due to RUN or ADD statements in the Dockerfile, container configuration change by ENV, CMD or USER) have been applied on top of the standard ubuntu:latest image to produce the final memcached one.
Docker history command details the patches that have been applied. They represent each non-comment line of the Dockerfile. Be aware that the oldest change is at the bottom of the stack, as in some kind of git log
output.
docker history 127.0.0.1:5000/ubuntu/memcached:latest IMAGE CREATED CREATED BY SIZE 4fd5c706f3a8 6 seconds ago /bin/sh -c #(nop) USER daemon 178.4 MB 43f04dd23753 8 seconds ago /bin/sh -c #(nop) CMD [/memcached.sh] 178.4 MB 34d94e18df44 9 seconds ago /bin/sh -c #(nop) EXPOSE [11211] 178.4 MB d60079a9c49b 11 seconds ago /bin/sh -c #(nop) ADD memcached.sh in / 28 B 55bc3a7d11cb 13 seconds ago /bin/sh -c apt-get install -y memcached 50.99 MB 86f4d2bdda8b 28 seconds ago /bin/sh -c ln -s /bin/true /sbin/initctl 16 B b5f46a480005 29 seconds ago /bin/sh -c dpkg-divert --local --rename --add 155.8 kB 8dbd9e392a96 9 months ago 128 MB
Now, it's time to push it on the local registry and run it:
$ docker push 127.0.0.1:5000/ubuntu/memcached [...] $ docker run -d -p 11211:11211 127.0.0.1:5000/ubuntu/memcached:latest
A truncated output of a ps afx
might give you something like:
/usr/bin/docker -d \_ lxc-start -n 84273abb1fcf73e6b538893c1b703a9c9e5911d36c1ba876e82db06d30f25259 -f /var/lib/docker/containers/84273abb1fcf73e6b538893c1b703a9c9e5911d36c1ba876e82db06d30f25259 | \_ /bin/sh -c cd /docker-registry && ./setup-configs.sh && ./run.sh | \_ /bin/bash ./run.sh | \_ /usr/bin/python /usr/local/bin/gunicorn --access-logfile - --debug --max-requests 100 --graceful-timeout 3600 -t 3600 -k gevent -b 0.0.0.0:5000 -w 4 wsgi:applic | \_ /usr/bin/python /usr/local/bin/gunicorn --access-logfile - --debug --max-requests 100 --graceful-timeout 3600 -t 3600 -k gevent -b 0.0.0.0:5000 -w 4 wsgi:ap | \_ /usr/bin/python /usr/local/bin/gunicorn --access-logfile - --debug --max-requests 100 --graceful-timeout 3600 -t 3600 -k gevent -b 0.0.0.0:5000 -w 4 wsgi:ap | \_ /usr/bin/python /usr/local/bin/gunicorn --access-logfile - --debug --max-requests 100 --graceful-timeout 3600 -t 3600 -k gevent -b 0.0.0.0:5000 -w 4 wsgi:ap | \_ /usr/bin/python /usr/local/bin/gunicorn --access-logfile - --debug --max-requests 100 --graceful-timeout 3600 -t 3600 -k gevent -b 0.0.0.0:5000 -w 4 wsgi:ap \_ lxc-start -n 621385033517cdc180d4f66c7f4687216602a070178fee15976caff2c325a924 -f /var/lib/docker/containers/621385033517cdc180d4f66c7f4687216602a070178fee15976caff2c325a924 \_ memcached
Two Docker containers are running, the first is the registry (which, as you can see, happens to be using gunicorn) and the second one, only runs a single memcached process. It's now possible to simply use the memcached service:
$ telnet 127.0.0.1 11211 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. version VERSION 1.4.13 quit Connection closed by foreign host.
Now what?
We just saw how to basically play with Docker containers and a local repository. The actions we performed can be summarized like this.
Note that we didn't get into details with the Docker save
and load
commands which are backup / restore features from / to a local tar archive. We didn't either explicitely use the commit
statement, which is used each time docker build
applies a line from a Dockerfile
and save it as a new image version.
In the next article, we will discuss about the configuration of Docker containers. Stay tuned!