Screen Shot 2014 03 17 at 12.25.19 PM How To Build A 2 Container App with Docker

Docker is fundamentally changing the way people do infrastructure in the cloud. However many developers I know are still confused about how to use it to do anything beyond a simple “Hello World” or a WordPress-all-in-one container with everything bundled into a single container, Apache, MySQL, nginx, etc. The power of Docker is in being able to ship and scale individual components or “containers”. However it is still a bit of a black art about how that works. There are a few awesome projects being started to make it easier to build multi-container apps like CoreOSSerfFlynn, Maestro, Deis, and Dokku.

But what if you want to do it without any fancy tools? How do you do it from scratch? I am not going to say this is the best way, but I would like to walk you through how I recently built a 2-Container app (WordPress in one container and MySQL in the other). In future articles, we will go into more complex setups and walk you through how to use some of the cool Open Source tools out there. This tutorial will guide you through 5 somewhat easy steps (Panamax is a new project we are working on to make this kind of tutorial unnecessary, you will be able to stitch together Docker containers in a simple web UI).

Step 1) Setup Docker Locally First, setup docker on your machine. If you are on a Mac, this is a simple 3-step process:

  1. Install VirtualBox
  2. Install Vagrant
  3. Install boot2docker (see below) Here is how to install

boot2docker which will install Docker on your Mac.

$ brew install boot2docker # if you are not on a Mac, go to https://www.docker.io/gettingstarted/ 
$ boot2docker init 
$ boot2docker up 
$ echo 'export DOCKER_HOST=tcp://localhost:4243' >> ~/.bash_profile 
$ source ~/.bash_profile
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

Step 2) Pull an Existing Container to Start With

Now that you have docker running on your development machine, let’s do something interesting. Let’s create your first single-container application. First, you go to the public Docker Index to find a container you want to use. Let’s search for WordPress. You will find about 30 results, but you only want to use a Trusted Repository.

NOTE: Most of the docker containers in the Docker Index are unsafe to use as-is. You have no idea how they were built, so you can’t be sure there isn’t malware on the images. If you want to use any public Docker images as a starting point, make sure to use a “Trusted Repository” (read more about trusted builds) and make sure to audit it’s Dockerfile before you use it.

One of the good trusted WordPress containers out there is managed by Tutum, a Docker-based hosting company that lets you run Docker containers and provides private docker registries and private docker images. The WordPress container you want to use is called tutum/wordpress and you can use it quickly and easily by doing the following:

$ docker run -d -p 80 tutum/wordpress /run.sh 
Unable to find image 'tutum/wordpress' (tag: latest) locally 
Pulling repository tutum/wordpress ef6168ed7674: 
Download complete 27cf78414709: 
Download complete [... more of the same ...] d713116991b8: 
Download complete 567a9d28bb372b23232ac2c1b527caf950b2e8095e2b6cbff0abba9e9ec10ec9
$ docker ps CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES     
567a9d28bb37 tutum/wordpress:latest /run.sh 13 seconds ago Up 12 seconds 0.0.0.0:49163->80/tcp, 3306/tcp suspicious_heisenberg 

Congratulations! You are now running WordPress in a single container with MySQL on the same container. Easy right? Want to see WordPress running for yourself? Run docker-osx ssh, and then run curl 0.0.0.0:49163/readme.html Now how do you get MySQL to run in its own container?

Step 3) Modifying an Existing Container

Now that you can use existing “Trusted Images”, let me show you how to modify them. Instead of running in daemon mode with the -d flag, use the interactive and TTY flags (-i -t) and specify bash to get into the container as root.

$ docker run -i -t tutum/wordpress bash 
root@dfa24049c264:/#  

Every wonder what /run.sh did in step 2’s run of the docker command? Let’s find out.

root@dfa24049c264:/# cat run.sh 
#!/bin/bash if [ ! -f /.mysql_admin_created ]; then /create_mysql_admin_user.sh fi 
exec supervisord -n 

Now that we don’t want MySQL running on the same container as WordPress, let’s modify this container to not run MySQL.

root@dfa24049c264:/# rm /etc/supervisor/conf.d/supervisord-mysqld.conf
root@dfa24049c264:/# touch /.mysql_admin_created 

In a new Terminal window, run docker ps again to find the container id of your interactive container.

$ docker ps 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 
dfa24049c264 tutum/wordpress:latest bash 9 minutes ago Up 9 minutes 3306/tcp, 80/tcp condescending_albattani  
110631a70e76 tutum/wordpress:latest /run.sh 13 minutes ago Up 13 minutes 0.0.0.0:49163->80/tcp, 3306/tcp cocky_einstein  

Now that we have the container id (dfa24049c264) we can commit our changes to the filesystem.

$ docker commit dfa24049c264 myuser/wordpress
852e9ccd1194f5abbcbfb13c61e54a31d5b716b20bec3d114437e824a39e168a

Now we have our own version of tutum’s WordPress with MySQL disabled. Let’s try it out. First we will stop our other containers and then we will start a new one with our newly committed image, myuser/wordpress.

$ docker stop dfa24049c264 
$ docker stop 110631a70e76 
$ docker run -d -p 80 myuser/wordpress /run.sh f1bc5c3bd28aac9a9452fdaeafb527b9616e5167797d56654dee625479c8d70c 
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 
f1bc5c3bd28a myuser/wordpress:latest /run.sh 7 seconds ago Up 6 seconds 0.0.0.0:49164->80/tcp jovial_nobel  

Notice that the 3306 port does not show as open in the new container when it did in the previous one, but that you can still curl the url curl 0.0.0.0:49164/readme.html

Step 4) Create Your MySQL Container Just like WordPress, you can get yourself a MySQL container from the Docker Index.

$ docker run -d -p 3306 tutum/mysql /run.sh
63a974062480cdcb93db788dc2ef72cb1f5cbfe2c180b9fdfb1b13c62753c106
$ docker ps
CONTAINER ID        IMAGE                                        COMMAND                CREATED             STATUS              PORTS                     NAMES
63a974062480        tutum/mysql:latest                           /run.sh                18 seconds ago      Up 17 seconds       0.0.0.0:49165->3306/tcp   cranky_babbage                     
f1bc5c3bd28a        myuser/wordpress:latest                      /run.sh                5 minutes ago       Up 5 minutes        0.0.0.0:49164->80/tcp     jovial_nobel    

Again, let’s go in and check out what run.sh does.

$ docker run -i -t tutum/mysql bash
root@a6fd073d27bb:/# cat run.sh
#!/bin/bash
if [ ! -f /.mysql_admin_created ]; then
    /create_mysql_admin_user.sh
fi
exec supervisord -n
 Look familiar? It should, the same tutum folks made this image. We don't need to remove any components out of this container, but we do want to make sure that the root username and password do not change. If you go back to your OS X terminal, we can find out what username and password were generated and commit the new container locally. 

$ docker run -d -p 3306 tutum/mysql /run.sh
63a974062480cdcb93db788dc2ef72cb1f5cbfe2c180b9fdfb1b13c62753c106
$ docker ps
CONTAINER ID        IMAGE                                        COMMAND                CREATED             STATUS              PORTS                     NAMES
63a974062480        tutum/mysql:latest                           /run.sh                18 seconds ago      Up 17 seconds       0.0.0.0:49165->3306/tcp   cranky_babbage                     
f1bc5c3bd28a        myuser/wordpress:latest                      /run.sh                5 minutes ago       Up 5 minutes        0.0.0.0:49164->80/tcp     jovial_nobel                       
$ docker logs 63a974062480
=> Creating MySQL admin user with random password
=> Done!
========================================================================
You can now connect to this MySQL Server using:

    mysql -uadmin -pqa1N76pWAri9 -h -P

Please remember to change the above password as soon as possible!
MySQL user 'root' has no password but only allows local connections
========================================================================
2014-01-20 21:56:10,100 CRIT Supervisor running as root (no user in config file)
2014-01-20 21:56:10,111 WARN Included extra file "/etc/supervisor/conf.d/supervisord-mysqld.conf" during parsing
2014-01-20 21:56:10,330 INFO RPC interface 'supervisor' initialized
2014-01-20 21:56:10,330 WARN cElementTree not installed, using slower XML parser for XML-RPC
2014-01-20 21:56:10,330 CRIT Server 'unix_http_server' running without any HTTP authentication checking
2014-01-20 21:56:10,330 INFO supervisord started with pid 1
2014-01-20 21:56:11,333 INFO spawned: 'mysqld' with pid 385
2014-01-20 21:56:12,536 INFO success: mysqld entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
$ mysql -uadmin -pqa1N76pWAri9 -h 0.0.0.0 -P 49165 -e 'create database wordpress;'
$ docker commit 63a974062480 myuser/mysql
c1c5e63d427cc533325e3870ab9ed439e8bfe2e95c333ec54657210708816bbd

Ok now we know what the username and password are and have created our own container with a static root username and password. Let’s stop the generic container and start our new custom MySQL server container.

$ docker stop 63a974062480 $ docker run -d -p 3306 myuser/mysql /run.sh 
$ docker ps 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 
5a840e255282 myuser/mysql:latest /run.sh 1 seconds ago Up 1 seconds 0.0.0.0:49166->3306/tcp angry_bardeen  
f1bc5c3bd28a myuser/wordpress:latest /run.sh 23 minutes ago Up 23 minutes 0.0.0.0:49164->80/tcp jovial_nobel

Step 5) Binding Your WordPress Container to your MySQL Container

Congratulations, you have reached the last step! All you have to do now is bind the two containers together. In order for the two containers to know about each other, we will need to link them using the docker -link flag. Before we do that, we will need to make another modification to our WordPress container so that it can pull credentials from environment variables.

$ docker run -i -t myuser/wordpress bash 
root@f74cc171d7d5:/# apt-get install curl 
root@f74cc171d7d5:/# curl -L https://gist.github.com/cardmagic/8530589/raw/21c5d2f0a4f661531502a705a797e0357d998c86/gistfile1.txt > /app/wp-config.php 

Now you go back to another OS X terminal (keeping the bash container running) and commit these changes to the container.

$ docker commit f74cc171d7d5 myuser/wordpress 
$ docker stop f74cc171d7d5 

Finally, it is time to start the myuser/wordpress container with a special -link flag and the environmental variable for the MySQL DB password.

$ export DB_PASSWORD=qa1N76pWAri9 
$ docker ps 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 
5a840e255282 myuser/mysql:latest /run.sh 21 minutes ago Up 21 minutes 0.0.0.0:49166->3306/tcp angry_bardeen
$ docker run -e="DB_PASSWORD=$DB_PASSWORD" -link angry_bardeen:db -d -p 80 myuser/wordpress /run.sh 
$ docker ps 
CONTAINER ID        IMAGE                                        COMMAND                CREATED             STATUS              PORTS                     NAMES 
2fb827d3c3a7        myuser/wordpress:latest                      /run.sh                2 seconds ago       Up 1 seconds        0.0.0.0:49167->80/tcp     ecstatic_thompson                     
5a840e255282        myuser/mysql:latest                          /run.sh                22 minutes ago      Up 22 minutes       0.0.0.0:49166->3306/tcp   angry_bardeen,ecstatic_thompson/db    

We now have a 2-container setup that are bound together. To see it in action, go to:

$ curl 

Conclusion

We can now create a 2-container application in Docker without even having to touch a Dockerfile. We can tie the containers together in a loosely coupled way. You can create multiple instances of the WordPress container that all talk to the same MySQL database. Note that this is not a production environment yet. The MySQL container’s data is ephemeral and we did not cover how to persist it yet (hint: look at the docker run -v flag).

Also note that in production we would want to docker push our myuser/wordpress and myuser/mysql images to a private Docker registry. We will show you how to run your own registry in an upcoming tutorial.

Next Week

In the next installment next week, we will talk about adding an nginx container to load balance and using Serf to automatically make all of the containers aware of each other. To automatically get an email when the next installment comes out, subscribe to our weekly newsletter that has Docker tips and news.

1x1.trans How To Build A 2 Container App with Docker
Follow me

Lucas Carlson

Chief Innovation Officer at CenturyLink
Lucas is the head of CenturyLink Labs. He publishes weekly Docker advice, tutorials, and open source software like Panamax (Docker Management for Humans) in this blog.
1x1.trans How To Build A 2 Container App with Docker
Follow me

Latest posts by Lucas Carlson (see all)

the author

Lucas is the head of CenturyLink Labs. He publishes weekly Docker advice, tutorials, and open source software like Panamax (Docker Management for Humans) in this blog.

  • Pingback: How To Build A 2-Container App with Docker | CT...

  • bmullan

    You mention that there are a few awesome projects being started to make it easier to build multi-container apps like CoreOS, Serf, Flynn, Maestro, Deis, and Dokku. But you failed to include or mention Canonical’s JuJu – https://juju.ubuntu.com/ JuJu can be configured with one command to use LXC to deploy application services url: https://juju.ubuntu.com/docs/config-local.html I’ve previously deployed an openstack service using juju and LXC.

    • cardmagic

      Not an intentional oversight! Thanks for the addition!

  • Pingback: Auto-Loadbalancing Docker with Fig, Haproxy and Serf | CenturyLink Labs

  • Pingback: My Very Best Ping

  • Wayne Walls

    I think you might need a “-L” switch on that curl command to make sure the redirect works correctly. Might have just been me, just wanted to throw it out there.

  • Bryan Lee

    thanks for the useful tutorial! There’s another here that I found interesting, http://blog.tutum.co/2014/02/06/how-to-build-a-2-container-app-with-docker-and-tutum/

  • http://kevinridgway.com Kevin Ridgway

    Port ‘49165’ should be changed to ‘49156’ in the ‘mysql -uadmin -pqa1N76pWAri9 -h 0.0.0.0 -P 49165 -e ‘create database wordpress;” command.

    In the curl command you have to add ‘-L’ to correctly handle the redirect that github has on their gist site for that wp-config file.

  • http://dev.svetlyak.ru Alexander Artemenko

    There is a mistake in “Step 2″. Please change “docker-osx ssh” to “boot2docker ssh” and a note that default ssh password is “tcuser”.

  • Pingback: Homepage

  • Pingback: URL

  • Jordan Day

    At the end of step 3, it shows the new container only exposing port 80 (via host 49164), but I’m still seeing port 3306 exposed:

    CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

    010e8a3d29d1 jordan/wordpress:latest /run.sh 7 minutes ago Up 7 minutes 3306/tcp, 0.0.0.0:49157->80/tcp trusting_kirch

    I presume the underlying wordpress image has updated since you wrote this article, but I wonder if there’s a way to “un-expose” a port on a container?

  • Avinash

    Hi I am new to docker i not able to ping google,com from following docker container with following details.
    Had made the docker0 equivalent to my network ip.
    docker ps
    CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
    c33dbd6511eb centos6-base:latest /bin/bash 20 seconds ago Up 19 seconds high_newton
    [root@puppet /]# docker inspect c33dbd6511eb
    [{
    “ID”: “c33dbd6511eb8805e9f9ebaa15d32f43967d048e3c50b3731eedcd712dfebe7a”,
    “Created”: “2014-06-11T10:34:59.154737236Z”,
    “Path”: “/bin/bash”,
    “Args”: [],
    “Config”: {
    “Hostname”: “c33dbd6511eb”,
    “Domainname”: “”,
    “User”: “”,
    “Memory”: 0,
    “MemorySwap”: 0,
    “CpuShares”: 0,
    “AttachStdin”: true,
    “AttachStdout”: true,
    “AttachStderr”: true,
    “PortSpecs”: null,
    “ExposedPorts”: {},
    “Tty”: true,
    “OpenStdin”: true,
    “StdinOnce”: true,
    “Env”: [],
    “Cmd”: [
    "/bin/bash"
    ],
    “Image”: “centos6-base”,
    “Volumes”: {},
    “WorkingDir”: “”,
    “Entrypoint”: null,
    “NetworkDisabled”: false,
    “OnBuild”: null
    },
    “State”: {
    “Running”: true,
    “Pid”: 7760,
    “ExitCode”: 0,
    “StartedAt”: “2014-06-11T10:34:59.452438921Z”,
    “FinishedAt”: “0001-01-01T00:00:00Z”
    },
    “Image”: “60431f2566f7e02ae91bae4db555683496c6a1ea2567e9134085ed7bc3568c94″,
    “NetworkSettings”: {
    “IPAddress”: “192.168.1.2”,
    “IPPrefixLen”: 24,
    “Gateway”: “192.168.1.253”,
    “Bridge”: “docker0″,
    “PortMapping”: null,
    “Ports”: {}
    },
    “ResolvConfPath”: “/etc/resolv.conf”,
    “HostnamePath”: “/var/lib/docker/containers/c33dbd6511eb8805e9f9ebaa15d32f43967d048e3c50b3731eedcd712dfebe7a/hostname”,
    “HostsPath”: “/var/lib/docker/containers/c33dbd6511eb8805e9f9ebaa15d32f43967d048e3c50b3731eedcd712dfebe7a/hosts”,
    “Name”: “/high_newton”,
    “Driver”: “devicemapper”,
    “ExecDriver”: “lxc-0.9.0″,
    “MountLabel”: “”,
    “ProcessLabel”: “”,
    “Volumes”: {},
    “VolumesRW”: {},
    “HostConfig”: {
    “Binds”: null,
    “ContainerIDFile”: “”,
    “LxcConf”: [],
    “Privileged”: false,
    “PortBindings”: {},
    “Links”: null,
    “PublishAllPorts”: false,
    “Dns”: null,
    “DnsSearch”: null,
    “VolumesFrom”: null,
    “NetworkMode”: “bridge”
    }
    }

    On physical machine following are the network details
    }][root@puppet /]# ifconfig
    docker0 Link encap:Ethernet HWaddr FE:2D:08:E9:11:91
    inet addr:192.168.1.253 Bcast:192.168.1.255 Mask:255.255.255.0
    inet6 addr: fe80::fcac:97ff:fe69:69b1/64 Scope:Link
    UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
    RX packets:155 errors:0 dropped:0 overruns:0 frame:0
    TX packets:42 errors:0 dropped:0 overruns:0 carrier:0
    collisions:0 txqueuelen:0
    RX bytes:5948 (5.8 KiB) TX bytes:2628 (2.5 KiB)

    eth0 Link encap:Ethernet HWaddr 08:00:27:C9:39:9E
    inet addr:192.168.1.252 Bcast:192.168.1.255 Mask:255.255.255.0
    inet6 addr: fe80::a00:27ff:fec9:399e/64 Scope:Link
    UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
    RX packets:1400259 errors:0 dropped:0 overruns:0 frame:0
    TX packets:575200 errors:0 dropped:0 overruns:0 carrier:0
    collisions:0 txqueuelen:1000
    RX bytes:249326906 (237.7 MiB) TX bytes:430728323 (410.7 MiB)

    eth1 Link encap:Ethernet HWaddr 08:00:27:F5:3D:C3
    inet addr:192.168.111.11 Bcast:192.168.111.255 Mask:255.255.255.0
    inet6 addr: fe80::a00:27ff:fef5:3dc3/64 Scope:Link
    UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
    RX packets:282052 errors:0 dropped:0 overruns:0 frame:0
    TX packets:146436 errors:0 dropped:0 overruns:0 carrier:0
    collisions:0 txqueuelen:1000
    RX bytes:406386684 (387.5 MiB) TX bytes:9272034 (8.8 MiB)

    lo Link encap:Local Loopback
    inet addr:127.0.0.1 Mask:255.0.0.0
    inet6 addr: ::1/128 Scope:Host
    UP LOOPBACK RUNNING MTU:16436 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:0
    RX bytes:0 (0.0 b) TX bytes:0 (0.0 b)

    vethAJHT2v Link encap:Ethernet HWaddr FE:2D:08:E9:11:91
    inet6 addr: fe80::fc2d:8ff:fee9:1191/64 Scope:Link
    UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
    RX packets:6 errors:0 dropped:0 overruns:0 frame:0
    TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
    collisions:0 txqueuelen:1000
    RX bytes:552 (552.0 b) TX bytes:872 (872.0 b)