php[architect] logo

Want to check out an issue? Sign up to receive a special offer.

Getting Started With Docker

Posted by on February 6, 2024

“Works on my machine” it’s a common refrain before closing out a bug ticket. This hides the fact that something is different between your development and production environments. There are a bunch of ways to resolve this problem but one of the easiest is to use a solution like Docker to make sure our development environment matches our production environment as closely as possible.

In this article, we’ll discuss what Docker is, why you need it, and how to get a basic development environment up and running.

 

 

What Are Containers?

In modern kernel versions, Linux provides access to a technology known as a container. A container is a sandboxed process running on a host machine that will be completely isolated from any other processes that might be running on the same host. These containers are portable and can be run on any OS and due to their isolation, they can run software at specific versions and with specific configurations. Because it’s just another process running on the Linux kernel, the overhead for this container is very small.

We can run these containers on local machines, in virtual machines, or a cloud provider’s infrastructure.

What Is Docker?

Creating containers can be a little challenging, and Docker provides the glue necessary to quickly and painlessly (mostly) create a container that you can use on one or more Linux-based computers. It does this by providing a set of command line tools for interfacing with the containers as well as a file format we can use to define our containers, what services they provide, and how they interrelate.

Ideally, we should only run one process at a time in each container, so we’ll need to create multiple containers for each of the “services” we need to run our application.

We can use a Dockerfile to define how the containers should be created and a docker-compose.yml file to define how our containers are related and what services they provide. We can then include these files in our source control so everyone can use them and have essentially the same setup if not exactly the same setup.

Docker allows us to create an image of your container at a specific point, which we can also share with everyone so the whole team can use the same development environment. You can then push this out to your production environment.

Because of this we can cure our development flow of the “works on my machine” mentality.

Some Vocabulary

There are a couple of things we need to discuss before we talk more about Docker and containers.

Image

A running container will use an isolated filesystem. An image is used to provide this isolated filesystem. The image must contain everything necessary to run our application including its dependencies, configurations, binary files, and so on. It will also contain information about the default command to run, environment variables, and any other data it might need.

The isolated filesystem is also reset every time the container is stopped so any changes to the filesystem will be lost. This is a good thing because it will make it easy to “reset” our application to a known good state.

Network

We can define networks that containers will use to connect to and communicate with each other. These networks will also be isolated so you can expect them to stay secure.

Volumes

Volumes provide the ability to connect specific filesystem paths of the container back to the host machine. If you mount a directory in the container, changes in that directory are also seen on the host machine. If you mount that same directory across container restarts, you’ll see the same files even after a reset.

How to Use Docker on Linux

Docker on Linux is easy to use as long as you have access to a modern Linux environment. In this example, we’ll be working through setting up the environment on Ubuntu 22.04 as it’s the current LTS version as of this writing.

To install Docker we need to run:

apt-get update
apt-get install docker.io docker-compose

I’m also going to create a directory to store our work.

mkdir docker-example && cd docker-example

How to Use Docker Everywhere Else

You might be saying to yourself that’s great Scott but I don’t use Linux every day so what do I do? In that case, you need to download Docker Desktop which will provide an application you can run when you need Docker. The application will transparently spin up a virtual machine (VM) on your computer which will then be used to host the Docker-based containers.

I should warn you that because you’re running the containers inside a VM there is a performance penalty but you may not even notice it depending on how fast your computer is.

After the Docker Desktop application is installed you’ll need to have it running for the next piece of our tutorial.

Let’s Setup a LAMP Stack

For our example today we’ll set up a very basic Linux, Apache, MySQL, and PHP (LAMP) stack.

I’m going to start out with a very basic “index.php” file that just outputs the phpinfo()

<?php
phpinfo();

Dockerfile

The first step is to create our Apache container. We’ll do this by creating a file named “Dockerfile” at the root of our project.

FROM php:8.3-apache
COPY index.php /var/www/html/index.php

Right now it just contains the line “FROM php:8.3-apache” which is telling Docker that we’re going to create a container using the image “php:8.3-apache” which contains a build of Apache configured to serve files on port 80 with PHP 8.3 installed. Then we want to copy our index.php file to /var/www/html so it can be served by Apache using the COPY command.

Now we need to build the image using the command docker build -t php8.3 .

~/docker-example# docker build -t php8.3 .
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
            Install the buildx component to build images with BuildKit:
            https://docs.docker.com/go/buildx/

Sending build context to Docker daemon  3.072kB
Step 1/2 : FROM php:8.3-apache
8.3-apache: Pulling from library/php
1f7ce2fa46ab: Pull complete 
48824c101c6a: Pull complete 
249ff3a7bbe6: Pull complete 
aa5d47f22b64: Pull complete 
e83ad87cf6a6: Pull complete 
92eeb6cb0068: Pull complete 
b3a08d032c4e: Pull complete 
0d4917eca7a8: Pull complete 
5f34393d3a2a: Pull complete 
ec40d4381201: Pull complete 
d51f7f116b07: Pull complete 
24aa8368a24e: Pull complete 
38972f936082: Pull complete 
Digest: sha256:c55d99c94f804ee54177ba00961d2441333b277a67e6a6901341cb251b47f638
Status: Downloaded newer image for php:8.3-apache
 ---> f3d702d507c3
Step 2/2 : COPY index.php /var/www/html/index.php
 ---> 1ecff99f5f07
Successfully built 1ecff99f5f07
Successfully tagged php8.3:latest

The first time we build the image, Docker will need to download all the images involved in php:8.3-apache and then copy our index.php over. If we rebuild it all of the images will already be cached so it will be much faster.

We can now run docker image ls to see our images:

~/docker-example# docker image ls
REPOSITORY   TAG          IMAGE ID       CREATED         SIZE
php8.3       latest       1ecff99f5f07   3 minutes ago   507MB
php          8.3-apache   f3d702d507c3   2 days ago      507MB

Next, we’ll use docker run to run our image and allow us to connect to port 80.

~/docker-example# docker run -p 0.0.0.0:80:80 php8.3
AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message
AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message
[Thu Nov 30 16:17:07.632546 2023] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.57 (Debian) PHP/8.3.0 configured -- resuming normal operations
[Thu Nov 30 16:17:07.633013 2023] [core:notice] [pid 1] AH00094: Command line: 'apache2 -D FOREGROUND'

I can now access index.php at the IP address of my test machine and see I do get the 8.3 phpinfo() page.

The way we ran the container puts the output on our terminal so we can see our access but this isn’t the best way to run it as we need to keep the connection open. We can get around this by running the container in the background by adding the “d” command line switch.

~/docker-example# docker run -dp 0.0.0.0:80:80 php8.3
4c50209c58502db19ebb9cc78975ba9e089d36f3759671d9ac98aba42c3823e3

This will output a string that is the container id. If we want to stop the container we need to run: docker container stop <container_id>

~/docker-example# docker container stop 4c50209c58502db19ebb9cc78975ba9e089d36f3759671d9ac98aba42c3823e3
4c50209c58502db19ebb9cc78975ba9e089d36f3759671d9ac98aba42c3823e3

Editing a File

Let’s alter our container so we can do some development work. To do this I’m going to change the Dockerfile so we install “vim” so we can edit our files.

FROM php:8.3-apache
COPY index.php /var/www/html/index.php
run apt-get update && apt-get install -y vim

Now I’m going to use the build command to rebuild the image and then run it using run command.

docker build -t php8.3 .
docker run -dp 0.0.0.0:80:80 php8.3

To access the container I’m going to use docker exec -it <container_id> /bin/bash to open a bash process inside the container.

Now I can edit the “index.php” file using vim /var/www/html/index.php. I’ll change it to output my name.

<?php
echo "Scott Keck-Warren";

I’ll jump back to my browser to see the results.

I’m going to exit the container, stop it, and start it again.

root@8151e91f3c8c:/var/www/html# exit
exit
root@ubuntu-s-1vcpu-1gb-amd-nyc3-01:~/docker-example# docker stop 8151e91
8151e91
root@ubuntu-s-1vcpu-1gb-amd-nyc3-01:~/docker-example# docker run -dp 0.0.0.0:80:80 php8.3
f771bd99e7277823af3ad5d829615f5b21d84e642d2f9a40443178b7dd7ca636
root@ubuntu-s-1vcpu-1gb-amd-nyc3-01:~/docker-example#

If I go back to the browser we’re back to the phpinfo() dump.

That’s how far we are away from messing up our environment and restarting to a known good state but how do we code this if it keeps resetting?

Docker-Compose

The solution to this is to set up a volume so we can edit the files outside of the container and have them automatically show up in the container.

To start I’m going to create a “docker-compose.yml” file at the root of my project and fill it with the contents below.

version: '3'
services:
    web:
        build:
            context: .
            dockerfile: Dockerfile
        ports:
            - '${APP_PORT:-80}:80'
        volumes:
            - '.:/var/www/html'

To launch our new setup we’ll run docker-compose up -d to start our images in detached mode. We’ve already built them so we don’t need to run docker-compose build.

root@ubuntu-s-1vcpu-1gb-amd-nyc3-01:~/docker-example# docker-compose up -d
Starting docker-example_web_1 ... done

Can now edit “index.php” with the echo "Scott Keck-Warren"; and see the changes in the browser.

Then we can restart the environment using docker-compose down and docker-compose up -d.

root@ubuntu-s-1vcpu-1gb-amd-nyc3-01:~/docker-example# docker-compose down
Stopping docker-example_web_1 ... done
Removing docker-example_web_1 ... done
Removing network docker-example_default
root@ubuntu-s-1vcpu-1gb-amd-nyc3-01:~/docker-example# docker-compose up -d
Creating network "docker-example_default" with the default driver
Creating docker-example_web_1 ... done

And see the changes are still there even after the reset.

Adding a Database

So now we want to do some real development and get a MySQL database involved. To do this we’re going to edit our docker-compose.yml file to include a mysql container and link them using a network.

version: '3'
services:
    web:
        build:
            context: .
            dockerfile: Dockerfile
        ports:
            - '${APP_PORT:-80}:80'
        volumes:
            - '.:/var/www/html'
        networks:
            - scotts-network
        depends_on:
            - mysql
    mysql:
        image: 'mysql/mysql-server:8.0'
        ports:
            - '${FORWARD_DB_PORT:-3306}:3306'
        environment:
            MYSQL_ROOT_PASSWORD: 'password'
            MYSQL_ROOT_HOST: '%'
            MYSQL_DATABASE: 'scottDatabase'
            MYSQL_USER: 'scottUser'
            MYSQL_PASSWORD: 'scottPassword'
            MYSQL_ALLOW_EMPTY_PASSWORD: 1
        volumes:
            - 'mysql-volume:/var/lib/mysql'
        networks:
            - scotts-network
networks:
    scotts-network:
        driver: bridge
volumes:
    mysql-volume:
        driver: local

The mysql container also has a volume which will be managed by Docker to provide a location for the MySQL files to be stored.

We also need to alter our Dockerfile to enable the pdo_mysql extension.

FROM php:8.3-apache
COPY index.php /var/www/html/index.php
run docker-php-ext-install pdo_mysql

I’m also going to alter the index.php to connect to the database using our connection information from the “docker-compose.yml”.

<?php
$host = "mysql";
$db = "scottDatabase";
$user = "scottUser";
$password = "scottPassword";

$dsn = "mysql:host=$host;dbname=$db;charset=UTF8";

try {
    $pdo = new PDO($dsn, $user, $password);

    if ($pdo) {
        echo "Connected to the $db database successfully!";
    }
} catch (PDOException $e) {
    echo $e->getMessage();
}

Now we need to rebuild our containers using docker-compose build so the PDO extension can be built and enabled.

Now we need to run docker-compose up which will then set up our MySQL container.

Just like this, we have a working LAMP stack. We could add other services like Redis or Memcache but we’ll leave that as an exercise for you.

What You Need to Know

  • Docker is a tool to build Linux containers
  • Use Dockerfile to define how our containers should be created
  • Use docker-compose.yml to define how our containers interrelate

Tags:
 

Leave a comment

Use the form below to leave a comment: