If you’re somewhat into technology chances are that you already heard about Docker. It is a platform which enables you to create universal containers that can be used independently of the underlying infrastructure. For example as a tool for local development, or in test environments, or also to serve your customers on the production environment. In this post we want to introduce Docker as a tool for the development process and our lessons learned.
Why to use Docker?
Developing mostly web-projects, so far Vagrant has been the tool of choice to create a local development environment replicating the production environment as close as possible. The goal behind this is to reduce the chance of side-effect caused by the underlying systems. With Vargant – or virtual machines in general – we can ensure that the same basic operating system and dependencies are used by all developers (e.g. PHP and MySQL on a Ubuntu system). Vagrant still serves this need very well.
Yet with the trend towards microservices, there hardly isn’t the “one” web application anymore. Instead there is a multitude of services working together. For example a frontend application, a backend application and a database. Trying to replicate this with virtual machines will require more system resources than most developer machines offer. This is where Docker plays one of its strengths: The overhead on computing resources is lower than with virtual machines, making it easier to run multiple services in the form of containers at once.
We found that this encourages developers as well to maintain the logical separation in a stricter way and aids the design in a positive way. Like Vagrant, Docker runs on all major platforms, being native on a Linux host, there are now native implementations on macOS (Docker for Mac) and Windows (Docker for Windows) as well.
Getting started with Docker
Tons of resources are available for Docker, their own Getting started gives a very good overview on the terms and basic concepts.
Docker Hub is a good starting point for new projects – it’s the central repository where most public Docker images are available. It works pretty much like a Package Manager: Anyone can publish their own images and everyone can pull images from there for further use. It holds images for most major programming languages and frameworks, e.g. PHP, Python, Java or Node.js. Those images offer a complete system with all necessary dependencies, often in different flavors. For example the PHP repository offers a CLI-image, one pre-configured with Apache, another one set up for FPM and some more.
In terms of local environments I would always recommend to use docker-compose. It is a wrapper around the CLI of Docker and makes the configuration of a container much easier by allowing to define dependencies and mounts in a human-readable way and taking care of configuration when starting a container.
The central part is a file named
docker-compose.yml: In this YAML-file we define which containers we want to have and how they should be configured. Running any
docker-compose command in a folder with this file will automatically be executed on those containers.
As we will use
docker-compose in the following examples, here are a few commands to get you started:
docker-compose upwill build and start all defined containers. By default you will see the log-output of all containers. Adding
-das parameter will start the containers in detached mode.
- Essentially it runs first
docker-compose buildwhich builds a container from an image
- And then
docker-compose startto actually start the container
- Essentially it runs first
docker-compose downwill stop all containers
- By default these commands will always affect all containers. To start or stop single ones, you can add the containername as an additional parameter, e.g.
docker-compose up myAwesomeContainerwill only start the container
myAwesomeContainerand not interfere with any of the others.
docker-compose pswill show all currently active containers
Example 01: Hello World (PHP)
A simple example of a PHP-script (placed at
src/index.php relative to the
docker-compose.yml) could be as easy as this
version: "3" services: myPhpContainer: image: php:7-cli working_dir: /var/www command: php -S 0.0.0.0:80 -t . ./index.php volumes: - ./src:/var/www ports: - "8080:80"
What is happening here? First, we define that our container (named “myPhpContainer”) is based on the official PHP docker image running the latest PHP 7 CLI version (see entry
image). Upon getting started (e.g. by running
docker-compose up) it will download that image and create a container from it. During the start process it will make sure to mount the local directory
src to the location
/var/www inside the container and to forward the port
8080 of the host system to port
80 of this container. So if you go ahead and enter
http://localhost:8080 in your browser it will be answered by port
80 of the container.
Once the container is running, it will change into the directory
/var/www (specified as
working_dir) inside the container and execute the command
php -S 0.0.0.0:80 -t . ./index.php (see
command), which starts PHPs built-in server.
If you now open
http://localhost:8080 in your browser, you can see the contents of your
index.php file. Since the directory
src/ is mounted into the container, any changes you locally make to the file
src/index.php will also be directly available from the container.
Example 2: A simple database
Taking this one step further it’s fairly easy to spin up whole applications, like WordPress. Due to the fact that Wordpress requires a database, we add a second container (running MySQL) and make the Wordpress container aware of it:
version: "3" services: wp: image: wordpress:latest ports: - "8080:80" depends_on: - db environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: dbuser WORDPRESS_DB_PASSWORD: dbpassword WORDPRESS_DB_NAME: database db: image: mysql environment: MYSQL_ROOT_PASSWORD: root MYSQL_USER: dbuser MYSQL_PASSWORD: dbpassword MYSQL_DATABASE: database
To make the
wp container aware of the
db container, we use the
depends_on property: Now docker-compose will make sure that the database is running before it starts Wordpress. And additionally the hostname
db is now resolved to that container on the Wordpress container.
environment allows us to define environment variables for each container. In this example we make use of the MySQL image’s ability to define initial User, Password, and Database. So on the first launch of the
db container the user
dbuser with the password
dbpassword and the database
database will be created. Since we pass those parameters also to the Wordpress container, it will use them directly for the setup process.
So if you launch the above example with a
docker-compose up you can head over to
http://localhost:8080 in your browser and will be greeted with the setup screen of WordPress. Yet the step to configure the database is omitted since that information has already been provided from the environment variables.
Where to go from here
The examples above will hopefully give you a starting point. From my experience there is a lot of trial and error involved, since each image has its own special features. Thanks to the architecture of Docker with intermediary images, build times are fairly low.
There are plenty of directions to go with Docker from here, so I will share a few points to keep in mind.
In the example above we started a database container – yet the data of this container is nowhere persisted. If the container goes away, so does your data. And from my experience Docker containers are more likely to be deleted and recreated since they are more lightweight. So spend a thought or two on what you want to persist outside the container for such services. In most cases the documentation of such data stores come with a section on how to achieve that and is usually done by creating and mounting a volume.
File Mount Performance
For now Docker has its limitations when it comes to projects with many files on Mac and Windows hosts. Currently we have a project with 13,000 files where we noticed a heavy performance decrease of 2-3 times compared to our existing Vagrant setup. Docker is working on this and hopefully resolves this soon. For that particular project we’re sticking with a Vagrant based development system.
Yet for smaller projects the comfort of Docker makes the hardly noticeable speed difference neglect-able. On a Linux host system I wasn’t able to notice any differences regarding speed at all.
Docker in Production
In the intro I already claimed that Docker can also be used to run as a production environment. One of the major promises of it is the idea of “build once, run everywhere” – the longstanding idea that you can create a system and run it wherever you want without having to make adjustments to the system itself.
In contrast to the approach present in this post, it’s necessary to copy all files into the container instead of mounting it. This will leave you with a local container having all resources necessary for your application. To get it into a production or test environment it’s best to push your image to a private registry.
Spinning up containers in various locations might eat up more resources than expected. So it’s a good idea to control them once in a while and make sure there aren’t any forgotten containers running in the background.
Using Docker in various projects and playing around with all sorts of containers might fill your disk fairly quickly. So if your host is getting low on disk space it’s worth to have a look at that list of containers and images as well (
docker images -a).
To conclude, there is also a nifty command to remove all unused resources occupied by Docker:
docker system prune will remove all stopped containers and other unused resources.
— happy containerizing.