This isn't a primer on Docker, containers, virtual machines, system resources, or anything like that. All of that has been written about extensively elsewhere. This is about getting local development environments up and running, fast.
When I stumbled upon Docker, at first I thought it was something like Composer or NPM because I could "install with Docker." So, I looked it up: "Docker is an application build and deployment tool [that] can package your code with dependencies into a deployable unit called a container."
Docker gives me something similar to what my laptop gives me: agility. I can work just about anywhere with my laptop. It's my setup. It has all sorts of configurations for my current and potential workflows. I feel confident with it.
Docker creates predefined (and/or customizable), contained software stacks. This means I can learn new technologies confidently without much "setup investment" and without interfering with other projects already hosted on my machine.
For example, getting a basic, no-frills Apache server with PHP running is an easy one-liner:
docker run -d -p 8080:80 -v "$PWD":/var/www/html php:apache
Let's dissect this command:
docker run
creates a container and then runs the processes we designate.-d
detaches the process so it can run in the background.-p 8080:80
exposes the container's internal port 80 and maps it to an external port. In this case, our app will become available at http://localhost:8080.-v "$PWD":/var/www/html
mounts a volume so it can be edited locally and changes seen live. It is essentially replacing a directory inside the container (right of the colon) with a local directory (left).php:apache
is the process we're running. Left of the colon is the process, and to the right is a tag that designates which PHP docker image to use to create the container. Here, we're using PHP's pre-configured PHP/Apache image. Other images using other servers, no servers, or different versions of PHP are also available.Run this command from a directory containing a simple PHP file with phpinfo()
and we should see the current configuration. And that's it, really. Edit the PHP file, refresh http://localhost:8080 and the changes will be there.
"But I need Apache configured with x, y and z." Don't worry, all we have seen is the nibbling of the rabbit, not the rabbit itself, nor the rabbithole. The configuration used to create the php:apache
image we ran can be dissected from its configuration file–its Dockerfile. This particular configuration is complex, but the reality is, we don't have to understand it to get going with Docker, just know that it's there and can be used to define more complex systems if needed.
Docker has many other repositories that are officially managed by their respective owners: nginx, Alpine, Redis, Ubuntu, httpd, MySQL, Node.js, etc. Not only are these images straight from their creators, we're also able to see every Dockerfile
for each tag so we can know exactly what the build is and how it's done.
So how can additions be made to this simple PHP setup? How can a MySQL container get working with this PHP container? Use multiple docker run
commands? Sure, but it can get messy. Instead, use Docker Compose.
Bundled with Docker's Mac and Windows installers (manual install on Linux), Docker Compose uses YAML to breakdown Docker commands into a single, human-friendly configuration file.
But first, the php:apache
image needs PHP's PDO extension installed so a connection can be made from the PHP application to test MySQL. Place this Dockerfile
in the root of the project:
FROM php:apache
RUN docker-php-ext-install pdo_mysql
Simple. It uses the php:apache
image, runs the command to install PDO, and creates a new image from which containers can be created.
The Docker Compose file (docker-compose.yml):
version: '3.1'
services:
app:
build: .
container_name: php_test_app
image: php_apache_pdo
ports:
- 8080:80
volumes:
- .:/var/www/html
db:
container_name: php_test_mysql
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: php_test
image: mysql:5.7
volumes:
- ./data:/var/lib/mysql
The docker-compose.yml
breakdown:
version: '3.1'
- Choosing a version of Docker Compose compatible with the version of the currently installed Docker Engine.services:
- Defining the containers and their configurations.app:
- Chosen name of the PHP container definition (could just as well be application, webapp, etc.).build: .
- Directs Docker Compose to build an image from a Dockerfile
located in the same directory as the docker-compose.yml
file.container_name: php_test_app
- Designating a name for the container itself. Otherwise, a random name will be generated.image: php_apache_pdo
- If building a new image, this will become the tag, otherwise use the standard image:tag combination (i.e., php:apache
).ports:
- 8080:80
volumes:
- .:/var/www/html
db:
- Chosen name of the MySQL container definition.container_name: php_test_mysql
- Important to note in this case is that this container name becomes our host when connecting to MySQL through the application.environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: php_test
image: mysql:5.7
- Using MySQL's version 5.7 image. No new images need to be built here.data
directory (created on the fly if necessary) in the root of our project.
volumes:
- ./data:/var/lib/mysql
Toss a PHP script that tests the database connection into the project's root directory as index.php:
$db = 'php_test';
$user = 'root';
$pass = 'password';
$host = 'php_test_mysql';
try {
$db = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "Connected successfully";
} catch (PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}
$req = $db->query("SELECT schema_name FROM information_schema.schemata");
foreach ($req->fetchAll(PDO::FETCH_ASSOC) as $record) {
$results[] = $record;
}
echo '<pre>'; var_dump($results); echo '</pre>';
Now fire up the server: docker-compose up -d
.
Bringing up http://localhost:8080 should result in "Connected successfully" and an array of databases.
When done with the setup, run docker-compose down
. Docker Compose will terminate all containers and remove them automatically. Clean and easy.
There's so much more to Docker, but as of now, this itself has saved me many hours of frustration when setting up multiple and varied environments on a single machine.
(Code available at https://github.com/nickstaroba/docker-dev-environments)