Docker and Docker Compose for PHP Development with GitHub and Digital Ocean Deployment
Tags: docker, php, github, digitalocean
Published: 2019-11-25 17:43:59

I have always missed some easy to follow tutorials on Docker, so I have decided to create it myself. I hope it will help you understand why Docker is such a popular tool and why more and more developers are choosing Docker over Vagrant and other solutions.

When it comes to PHP development, you have basically three options on how to approach the problem of preparing a development environment for your project. The oldest way is to install individual services on your development machine manually. With different versions of these services on staging and production environments, you can get into dependencies problems rather quickly. Also, it’s quite a challenge to manage different projects with different requirements on a single computer.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*Rq0p1Drq3VCDT1f0.jpg)

You can solve this problem with virtual machines. Download VirtualBox and set up your environment individually for every project so they won’t interfere because they will be totally separated. When you need to deploy your work along with your environment to the remote server, you will just provision the whole virtual machine. Vagrant can help you with this process because it allows you to deploy your project directly. For example to a DigitalOcean Droplet. But the problem is that you are working with the full-blown operating systems even though they are virtualized.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*gQ-FncmmdWdrAYeo.jpg)

What if there is another way? What if you don’t need full operating system encapsulated in virtual machines to keep your projects separated and yet you would be able to have the same development environment everywhere, on your local machine, on the testing server and even on the production server. Welcome to the world of Docker!

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*jfIk9mrphZ7HYgfx.jpg)

To better understand the difference between Docker and VM-based solution, take a look at the image below:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*ip5rAUjldxn_RF_9.jpg)

Docker can help you as a developer in three areas by:

1.  eliminating the “it works on my machine” problem once and for all because it will package dependencies with your apps in the container for portability and predictability during development, testing, and deployment,
2.  allowing you to deploy both microservices and traditional apps anywhere without costly rewrites by isolating apps in containers; this will eliminate conflicts and enhance security,
3.  streamlining collaboration with system operators and getting updates into production faster.

Interested? Great! Let’s give it a try.

### I will make you a full-stack web developer in just 12 hours!

This tutorial is a part of a much larger and far more detailed online course available for free on <a href="https://skl.sh/3eP78uy" target="_blank">Skillshare</a>. If you're new to Skillshare, you'll get premium access not only to this course for free but to **all of my other courses as well and for a good measure, to over 22.000 courses** currently hosted on Skillshare. 

You won't be charged anything for your first 2 months and you can cancel your membership whenever you want within that period. Consider it a 2 months worth of premium education for free. It's like Netflix, only for life-long learners :-)

<a href="https://skl.sh/3eP78uy" target="_blank">![](https://codewithjan.com/images/posts/twdc-skillshare-cover-play.png)</a> 

### Docker Installation

This one is easy, all you need is download a package for your operating system. In my case, I downloaded Docker for Mac. You will install Docker as any other application. Once you see the Docker happily humming in your top bar, you can start building awesomeness with me!

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*4_7uhPGJcPDfc8SI.jpg)

I will use Atom (www.atom.io) editor along with the terminal-plus package. If you want to follow my setup, be aware that you need to tweak the terminal-plus a bit to work. There is some minor issue, but it can be easily fixed like this:

1.  Go to **~/.atom/packages/terminal-plus/package.json** and locate the dependencies section.
2.  Remove the commit id (#……) at the end of the pty.js entry:   
    “pty.js”: “git+https://github.com/jeremyramin/pty.js.git**#28f2667**”   
    becomes   
    “pty.js”: “git+https://github.com/jeremyramin/pty.js.git”
3.  Go to terminal and run these commands:

    ```
    cd ~/.atom/packages/terminal-plus/
    npm install
    apm rebuild
    ```

Restart the Atom editor and terminal-plus should work now! Of course, you can choose your own text editor and terminal client. Everything will work the same.

### Create your first Docker image

The easiest way to create a Docker image is with the **Dockerfile** which is something like a recipe for building an **image**.

*NOTE: Image is something like a blueprint. You can use one blueprint to create many objects like cars or houses. Similarly, you can use one image to create many containers.*

Let’s take a look at how easy it is to create a development environment based on PHP 5 and Apache webserver.

Create a new folder on your Desktop and name it **docker-apache-php5**.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*VfCy8EZtpEHMWBe9.jpg)

Inside **docker-apache-php5** folder, create new folder called **src** where we will add a new file called *phpinfo.php*

Create this file and put this code inside:

```php
    <?php  phpinfo(); ?>
```

This is a very simple php code that will call just one simple function, but this function will output a nice table with a detailed configuration of our development environment we are just creating.

Create another file, this time directly inside **docker-apache-php5** folder and call it **Dockerfile** (just like this, no extension). Inside this file, we will write some directives for Docker.

Your directory structure should look like this now:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*InSB7a2grGSsS3be.jpg)

We want to build our development environment on PHP 5 and Apache webserver. The best way is to start with the image already available. In the case of PHP, there is probably no better source than the official image.

Docker images are available on Docker Hub. Sign up for a free account and once you are in, you can search for images.

Go ahead and type **php** in the search box.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*k0ZL7qfnq7aKBbjT.jpg)

Select **php official** image and let’s take a look at the details:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*G93xxjA2iINswIq7.jpg)

We want to use **5.6-apache**, version which is PHP 5.6 including the Apache webserver. This is convenient because we don’t need to install Apache separately.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*dXDNGBU32i6Vk-7I.jpg)

Go ahead and click on the link [(*5.6/apache/Dockerfile*)](https://github.com/docker-library/php/blob/e573f8f7fda5d7378bae9c6a936a298b850c4076/5.6/apache/Dockerfile) which will get you to GitHub Repository of this image. Just take a look at the Dockerfile.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*aGo138OGoAGAhEec.jpg)

Don’t panic, our Dockerfile will have just a couple of lines because we will take advantage of the hard work of the PHP team and use their image. This doesn’t mean that you can’t just sit down and write your own image based on Debian Linux, but why would you want to waste your time, when you can just use what’s already done?

In order to use this image, go to your Dockerfile and write this line of code:

```
    FROM php:5.6-apache
```

This says that our own image will be based on the **5.6-apache** image created and maintained by the PHP team. Sweet.

Type this line below:

```
    COPY src/ /var/www/html/
```

Make sure there is a space between **src/** and **/var**. It’s very important! Now, this line says that we want the content of the **src** folder we have created a few minutes ago, to be copied to **/var/www/html/** but you might wonder why and where it is located.

This is the folder structure that will be created while our image will be built or more specifically while we will create a container from that image. I showed you the Dockerfile for **5.6-apache** image on purpose. Remember the line **FROM debian:jessie**?

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*_7UCBHzKxssWwSrq.jpg)

Our image will be based on a **5.6-apache** image, but even this image is based on another image. It this case, it is **debian:jessie** image. So basically, the PHP team grabbed **debian:jessie** image and added their own modifications with their Dockerfile, like we are adding our own modifications to **5.6-apache** image with our Dockerfile.

The point is that we are all adding layers to the basic **debian:jessie** image which is a Linux distribution and as you probably know, the Linux file system starts with root (**/**) followed by specific subfolders. Mac OS is based on UNIX and it works similarly. Your Desktop is actually located in **/Users/your-name/Desktop**. In my case, it is **/Users/zavrelj/Desktop**.

Now, for web content, Apache web server uses a directory called **html** which is stored inside **www** directory inside **var** directory. That’s how it is and because we know it, we can say that once our image with Apache web server is initialized or spun up to create a container, we can safely copy the content of our **src** folder to **/var/www/html** folder on Debian because it will be there since the Apache web server will create it during its own installation.

Give yourself a pause and let this all sink. It’s a really important concept.

Ok, the last line we will add to our Dockerfile looks like this:

```     
    EXPOSE 80 
```

It says that we want the port 80 to be available for incoming requests.

Your Dockerfile should look like this now:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*8o82f9L3qwGBQwHW.jpg)

Once you have this, save the changes, open your favorite terminal app. In the terminal, set your **docker-apache-php5** folder as a working directory. I expect you to know how to work with the command line.

If you don’t just open terminal and type

```
    pwd
```

It will show you your current working directory. If have you followed me step by step so far and you are on Mac computer, this command should get you to the right directory:

```
     cd ~/Desktop/docker-apache-php5 
```
Copy this code and paste it into the terminal, then hit Enter. To make sure, you are in the right directory, type **ls** in the terminal and hit Enter. If you see something like this, you’re good to go:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*pt19bUum6A8AC-TJ.jpg)

It’s important to be in the right directory, where the Dockerfile is saved. We will now build our image from the Dockerfile.

Type this line in terminal:

```
     docker build -t php-image . 
```

This command will build our image, **\-t** option lets you give the image a custom name, in our case, it will be **php-image** because I want you from the very beginning to be able to make a distinction between images and containers. Finally, the dot at the end of this command means that the Dockerfile is located in the current directory. That’s why we wanted to get there!

If everything went right, you should see something similar in your terminal:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*kj6LNWPMjW6O2YgV.jpg)

Docker has just created an image and assigned it an ID, in my case **576a14c36bc9**.  To see the list of all your images, just type this command in your terminal:

```
     docker image ls 
```

As you can see, there are two images, and they are sorted by the date of creation. The first one is our image we have just built, the second one is the image Docker pulled from Docker Hub. As you can see, its name is **php** and the tag is **5.6-apache**, together it makes **php:5.6-apache** which is exactly what we wrote in the first line of our Dockerfile! Docker needed to pull this image first in order to create our own image. That’s why we have two images even though we have created only one.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*8GzIRMiioETQzPNO.jpg)

Now we need to create a container from our **php-image**. Our image is just like a snapshot. To be able to actually work with your services like PHP, you need to spin up the container from that image.

Type this in your terminal:

```
     docker run -p 80:80 -d --name php-container php-image 
```

This will create a container from our **php-image**.

**\-p 80:80** is port mapping, remember how we exposed 80 in the Dockerfile? Well, now we need to tell the container to use the exposed port 80 and deliver its content to the port 80 of our localhost,

**\-d** stands for a detached mode which will bring the process to the background so you can still use the same terminal window,

**\--name** allows us to give our container name of choice, otherwise, Docker would pick one for us randomly.

And finally, at the end of this command is the name of the image from which we want to create our container.

Remember that the name must be put only after all options! To make sure that your new container is up and running, type this command in terminal:

```
    docker ps
```

You should see this:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*K7Vz9_EHLisf96dV.jpg)

You can see the container’s name, the image it was created from, ports, ID and status.

Since our container is waiting for some work, let’s make it do its job! Open your web browser and type **localhost/phpinfo.php**

You should get this:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*1Q0J9DJdWgFEDoF8.jpg)

It means that everything works and we are running PHP 5.6.30 on our local webserver! Great work!

### Adding database

Unfortunately, this container won’t work with a database because all we have is PHP and Apache webserver. To add a database server to our development environment, like MySQL for example, we would have to create a container for a database and connect it to our **php-container**, thus those two containers or rather services inside those containers could talk to each other.

Let’s take a look at how this can be done. We will start again in Docker Hub and search for mysql:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*EOwc7q7ifguhpNov.jpg)

And sure enough, there is an official repository maintained by MySQL team. Let’s create our own **mysql-container** by running this code:

```
     docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=secret -d mysql:latest 
```

When you run this code, Docker will first look for **mysql:latest** image on your computer. If it’s not available, it will pull it from Docker Hub first and then spin up the container from it. This is very important because Docker is trying to save your disk space. If **mysql:latest** image is already on your computer, Docker will use it instead of downloading yet another copy.

Remember this, we will come back to this concept later.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*y2zsdQSjE33rMs7H.jpg)

Type **docker ps** again. You should now see two containers and both are running.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*YV7sxjXIvq0rxoXn.jpg)

This demonstrates, that you can immediately spin up a container from an already existing image. Only if you want to create your own image, you need to actually build it first and run it later to spin up a container from it.

Let’s create some mysql code to see if mysql is working. Since we run PHP 5.6, we can use **mysql\_connect** function. Even though it’s deprecated in PHP 5 and completely removed from PHP 7, for our testing purposes, it will be just fine.

In **src** directory, create a new file named **mysql.php** and place this content in it:

```php
     <?php  
      
     $database = "users";  
     $user = "root";  
     $password = "secret";  
     $host = "mysql";  
      
     $db = mysql_connect("$host","$user","$password");  
      
     if (!$db) {  
      echo "Cannot connect to the database server";  
     }elseif ($db && !(mysql_select_db($SQL_DBASE, $db))) {  
      echo "Sucessfully connected to the database server! Database Users selected!";  
     }  
      
    ?> 
```

This is a very simple php code. It tries to connect to the database server with the credentials we provided. If the connection cannot be established, it will display an error. If the connection is successful and the database **users** exists, it will display a success message.

Now, try to go to **localhost/mysql.php** in your web browser. You should get this message:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*uGXi8m8Jr3CtKZa-.jpg)

Our **mysql.php** file can not be found, even though it is in the same directory as **phpinfo.php** file and this one can be found just fine.

The problem is that even though we added a new file and thus changed the content of our project, we are still running the old **php-container** based on the original **php-image** which has no clue about the changes we have just made.

To fix this, we need to rebuild our **php-image** and spin up a new container from this updated image. If you think now that this is a lot of hassle, it is, but just for now. You will truly appreciate a feature called **volumes** I will introduce later, once you go with me through this hell.

Stop the **php-container** we have created from **php-image** by running this command:

```
     docker stop php-container 
```

To list all containers, even those that are not running, use **docker ps -a** command. You can see that **php-container** has exited:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*bv4Ee3zcixpimT15.jpg)

Once the container is stopped, we can remove it with this command:

```
     docker rm php-container
``` 

You can remove the container even while it is running, in that case, you need to add **\-f** option to the end of the command above. Once the **php-container** is removed, you can remove the **php-image** as well. If you tried to remove **php-image** while the **php-container** still existed, Docker would protest.

```
     docker rmi php-image
``` 

Ok, now we can rebuild our **php-image** again and the only reason for that is to copy our new **mysql.php** file into **/var/www/html** folder. Remember the instruction from Dockerfile? Here it is again: **COPY src/ /var/www/html/**

This is why we did all of this, to get our new **mysql.php** copied from **src** folder to **/var/www/html** folder. There is another, way better solution, though, and we will get to it soon. Let’s build our image again:

```
     docker build -t php-image .
``` 

and spin up the updated container from it:

```
     docker run -p 80:80 -d --name php-container php-image
``` 

Now, navigate to **localhost/mysql.php** from your web browser. The file apparently exists, but we have another problem:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*JfCpCV3qxrNnPPs0.jpg)

PHP image is very lightweight, it doesn’t usually include PHP extensions and mysql is one of those missing. That means that PHP doesn’t know about any function called **mysql\_connect()**. To fix this, we need to add **mysql** extension to our **php-image** first. But don’t be scared, we won’t undergo the same painful process again.

You can directly rebuild the image and then run the container without the painful process of stopping the container, removing the container and rebuilding the image. But I didn’t tell you sooner because I wanted you to try all these commands so you know how to manage containers and images. I hope you will forgive me this pesky move 🙂

Go to your Dockerfile and add this line at the bottom:

```
     RUN docker-php-ext-install mysql 
```

This will add mysql extension to our PHP image. Now just run this command in terminal:

```
     docker build -t php-image . 
```

You can see that in Step 4/4, a mysql extension has been added to our image:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*pfNEkIE-wzJ5307E.jpg)

If you list all images with **docker image ls**, you can see that **php-image** has been created only a few seconds ago. This means that if you build the image with the same name, the original image is overwritten with the new one.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*05g0naxZp5Fe8vEw.jpg)

The same can’t be done with the container, though. If you try and run this command now while the original container is still running…

```
     docker run -p 80:80 -d --name php-container php-image 
```

…you will get this error message:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*WGD2nnHSo7UXTF0V.jpg)

You need to stop and remove the currently running **php-container** first. As I mentioned already, you can do this at the same time by using -f option:

```
     docker rm php-container -f 
```

Now you can spin up the **php-container** again, but this time the updated **php-image** will be used.

```
     docker run -p 80:80 -d --name php-container php-image 
```

*NOTE: You might wonder if you could just spin up a container with a different name and kept the original one running.  
You could do that! The problem is that you would have to map a different port as well because* ***port 80*** *would be still taken by the original* ***php-container****, thus unavailable for new mapping.  
This could be of course solved by using for example* ***\-p 81:80*** *instead of* ***\-p 80:80****. Finally, you would have to explicitly type the port to the web browser like this:* ***localhost:81/mysql.php****.*  
***Port 80*** *is the default one, that’s why it doesn’t have to be written explicitly, unlike other ports.*

OK, navigate to **localhost/mysql.php** from your web browser again. Even though we get another warning now, we are getting closer because the new error message comes directly from **mysql\_connect()** function. That means that it exists and PHP knows about it. But it seems like there is a problem with a network address:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*I4gnxiuZveXq4puv.jpg)

The reason for this error is the fact that we have two separate containers. One for PHP (**php-container**) and one for MySQL (**mysql-container**). And they don’t know about each other, they don’t talk to each other. Let’s fix this. Stop **php-container** once again and remove it at the same time:

```
     docker rm php-container -f 
```

Run this code:

```
     docker run -p 80:80 -d --name php-container --link mysql-container:mysql php-image 
```

You are already familiar with this code except for the link part. This says that we want to link our **php-container** with **mysql-container**. Now, navigate to **localhost/mysql.php** from your web browser, this time you should see this:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*7dTBPS6FgX8p10b1.jpg)

Perfect! Now, in order to be able to modify the content of our **src** folder without the need to rebuild images all the time, we will add **\-v** option to our docker run command. So for the last time, stop and remove php-container:

```
     docker rm php-container -f 
```

Run it again with this new option:

```
     docker run -p 80:80 -d -v ~/Desktop/docker-apache-php5/src/:/var/www/html/ --name php-container --link mysql-container:mysql php-image 
```

This option should be quite familiar. We used something similar in our Dockerfile to tell our image to copy the content of our **src** folder to the Apache webserver default directory inside the container.

Well, this time, we will create the volume, which means that those two locations will be in sync. Actually, we will mount our folder saved in Desktop to the location inside the container. Once you make any kind of change in **src** folder, it will be automatically available in **/var/www/html** folder in Apache webserver.

Let’s test this! Go to your **mysql.php** file and add “AMAZING!” at the end of the echo like this:

```php
    echo “Successfully connected to the database server! Database Users selected! **AMAZING!**“;
```

Save the file and refresh the browser! Isn’t that amazing? 🙂

### Docker Compose

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*VNJIHdALyPrMlhkV.png)

So far, we did it all manually. We configured and created images, we created containers and link them together. If you work with two or three containers, it is doable, even though we have spent quite some time with this. However, if you need to set up the environment with many more containers, it will become very tedious to go through all those steps manually every time.

Luckily, there is a better way. Docker Compose is a tool for defining and running multi-container Docker applications. It allows you to create a YAML configuration file where you will configure your application’s services, and define all the steps necessary to build images, spin up containers and link them together. Finally, once all this is done, you will just set it all in motion with a single command.

Let’s take a look at how this works. This time, we will create the LEMP stack which will consist of Linux, PHP 7, Ngnix, and MySQL. It is generally recommended to have one process, or microservice per container, so we will separate things here. We will create six containers and orchestrate them with Docker Compose.

As we already did in the previous section, we will again use official images and extend them with our Dockerfiles. First, let’s delete all containers and images, so we can start with a clean slate.

To list all containers:

```
     docker ps -a 
```

To delete containers:

```
     docker rm php-container -f  
     docker rm mysql-container -f 
```

To list all images:

```
     docker image ls 
```

To delete images:

```
     docker rmi php-image:latest -f  
     docker rmi php:5.6-apache -f  
     docker rmi mysql:latest -f 
```

**\-f** option will force deletion even if the container is running or the image is in use.

If by any chance you won’t be able to delete container or image by its name, use its ID instead. This is actually the only viable alternative if you happen to have an image with no name:  <none> 

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*A-NhzY64B4wxW1XY.png)

List all containers with:

```
     docker ps -a 
```

and all images with:

```
     docker image ls 
```

All clear? Great! Let’s begin! Go to your Desktop and create a new folder called **docker-ngnix-php7**.

### Nginx

Let’s start with a web server. Instead of Apache, we will use Nginx this time. First, we will check if there is any official image on Docker Hub. And sure enough, here it is:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*WYm6VTjMvJmbHiG5.png)

We will choose the tag **latest**, so I hope you remember, that the name of the image and the tag go together like this: **nginx:latest**

Now, create a new file in your **docker-nginx-php7** directory and save it as **docker-compose.yml**

Inside, write this:

```
     nginx:   
      image: nginx:latest  
      container_name: nginx-container  
      ports:   
       - 80:80 
```

This should be somewhat familiar. Remember when we ran mysql image? We used this command in the terminal: **docker run -p 80:80 -d --name php-container php-image**.

Now, instead of running this command, we will take the options and save them in a configuration file. Then, we will let Docker Compose run commands for us by following the instructions in this file. Save the file. This is what it should look like:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*8jG6vnXpKHpd5Z49.png)

Go to the **docker-nginx-php7** directory in your terminal and run this command:

```
     docker-compose up -d 
```

**\-d** option still means detached, nothing new here.

Docker Compose will pull Nginx image from Docker Hub, create a container and give it a name we specified. Then, it will start the container for us. Docker Compose will do all of these steps automatically.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*CdicIv75BST4DK7o.png)

I gave the container a specific name just for educational purposes here, so we can easily identify it. But it’s not a good practice in general because container names must be unique. If you specify a custom name, you won’t be able to scale that service beyond one container, so it’s probably better to let Docker assign automatically generated names instead. But, in our case, I want you to understand how things are working.

Use the familiar **docker ps** command to see the list of running containers. Write down the IP address assigned to **ngnix-container** and navigate to this address with your web browser. You don’t have to write the port number since 80 is a default value.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*1o-WAVxqTSFlqnJZ.png)

You should see your Nginx web server running:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*8fUvcF0TCRmhTzt3.png)

That was easy, right?

### PHP

Let’s say that we want to add the PHP to the mix and we want it to be automatically downloaded, configured and started. We also want to modify our Nginx web server a bit. You know the drill. If you want to modify the official image and add your own changes, you need to use Dockerfile as we already did in the previous section.

Let’s do this again. First, we will create a new directory inside our **docker-ngnix-php7** folder and name it **nginx**. In this directory, we will save a new **Dockerfile**. Next, we will create a new **index.php** file which will be saved in **www/html** directory inside **docker-ngnix-php7** folder with this content:

```html
     <!DOCTYPE html>  
     <head>  
      <title>Hello World!</title>  
     </head>  
       
     <body>  
      <h1>Hello World!</h1>  
      <p><?php echo 'We are running PHP, version: ' . phpversion(); ?></p>  
     </body>
``` 

This simple page will help us test if PHP is running.

*NOTE: If you use Atom editor, you can create* ***new*** *file and the whole new directory structure at the same time! Just click with the right mouse button on the name of* ***docker-nginx-php7*** *folder in left pane in Atom, choose* ***New File*** *and instead of typing just the name of the file, type the whole path* ***www/html/index.php****. Atom will create the file for you and both directories as well!*

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*7HgWp2itNFxa1hoT.png)

Your folder structure should look like this now:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*zqUL8p1LzAjix6qx.png)

To configure our Nginx web server, we will use **default.conf**, so create this file and save it in **nginx** folder. Now add this content inside **default.conf** and save it:

```
     server {  
      
     listen 80 default_server;  
     root /var/www/html;  
     index index.html index.php;  
      
     charset utf-8;  
      
     location / {  
      try_files $uri $uri/ /index.php?$query_string;  
     }  
      
     location = /favicon.ico { access_log off; log_not_found off; }  
     location = /robots.txt { access_log off; log_not_found off; }  
      
     access_log off;  
     error_log /var/log/nginx/error.log error;  
      
     sendfile off;  
      
     client_max_body_size 100m;  
      
     location ~ .php$ {  
      fastcgi_split_path_info ^(.+.php)(/.+)$;  
      fastcgi_pass php:9000;  
      fastcgi_index index.php;  
      include fastcgi_params;  
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;  
      fastcgi_intercept_errors off;  
      fastcgi_buffer_size 16k;  
      fastcgi_buffers 4 16k;  
    }  
      
     location ~ /.ht {  
      deny all;  
     }  
    } 
```

Now back to Dockerfile for Nginx. Write these two lines in it and save the changes:

```
    FROM nginx:latest   
    COPY ./default.conf /etc/nginx/conf.d/default.conf 
```

This means that we will start with the default nginx image (**nginx:latest**), but then, we will use our own configuration we have just saved in **default.conf** and copy it to the location of the original configuration. Now we need to tell Docker to use our own Dockerfile instead of downloading the original image and since Dockerfile is inside the nginx directory, we need to point to that directory. So instead of using **image: nginx:latest**, we will use **build: ./nginx/**

We will also create volumes so the Nginx web server and PHP as well can see the content of our **www/html/** directory we have created earlier, namely our **index.php** file which sits inside. This content will be in sync with the container’s directory **/var/www/html/** and what’s more important, it will be persistent even when we decide to destroy containers.

Next, we will create a new **php-container** using original PHP image, this time PHP 7 FPM version. We need to expose **port 9000** we set in **default.conf** file because the original image doesn’t expose it by default. And finally, we need to link our **nginx-container** to **php-container**. After implementing all those changes, our modified **docker-compose.yml** will look like this:

```
     nginx:    
       build: ./nginx/  
       container_name: nginx-container  
       ports:  
         - 80:80  
       links:  
         - php  
       volumes:  
         - ./www/html/:/var/www/html/  
      
    php:    
      image: php:7.0-fpm  
      container_name: php-container  
      expose:  
        - 9000  
      volumes:  
        - ./www/html/:/var/www/html/ 
```

*NOTE: You might wonder what is the difference between* ***ports*** *and* ***expose****. Exposed ports are accessible only by the container to which they were exposed, in our case* ***php-container*** *will expose port 9000 only to the linked container which happens to be* ***nginx-container****. Ports defined just as ports are accessible by* ***the host machine****, so in my case, it would be my MacBook or rather the web browser I will use to access those ports.*

Even though our **nginx-container** is still running, we can run this command:

```
     docker-compose up -d 
```

This time, Docker will pull **php:7.0-fpm** image from Docker Hub and create a new image based on the instructions in our Dockerfile.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*3lKirU9-ZaZrU-yH.png)

As you can see, Docker is warning us that it built the image for nginx service only because it didn’t exist. This means that if this image already existed, Docker wouldn’t build it and it would use the existing image instead. This is very important because even though you will change Dockerfile in the future, Docker will ignore those changes unless you specifically say that you want to rebuild the existing image by using the command **docker-compose build**.

Go ahead and take a look at the list of all images:

```
     docker image ls 
```

You should see the official nginx image, official php image that has just been pulled and finally, our modified version of the official nginx image which name is **dockernginxphp7\_nginx**. This name is based on the name of the directory where our **docker-compose.yml** file is saved. The last part of its name comes from the name of the image from which our image is derived, in our case **\_nginx**.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*YADB-J_3TjiSgj3d.png)

**docker ps** will show you two containers running:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*AJIqdzngO0UgZCU1.png)

*NOTE: If your* ***nginx-container*** *is not running, use* ***docker logs nginx-container*** *command to see what is the problem. Very probably, it will be some kind of typo in* ***default.conf*** *file.*

Even though we didn’t stop the original **nginx-container** based on the official nginx image, it’s not only stopped, it’s completely gone. Instead, we have our new modified nginx-container running, but this one is spun up from **dockernginxphp7\_nginx** image. If you go back to your web browser and refresh the page, you should see this:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*UbaubdRPURsGx_RY.png)

Let’s see if the mounted directory works as expected. Go to your **index.php** file and write **AMAZING!** inside the h1 tag like this:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*ctuZRwbCHFDPh2ix.png)

When you refresh the page, **AMAZING!** will appear:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*DInNUzCXOV3ZHEUP.png)

One last thing before we move to the database. As you might have noticed, we have mounted the same directory **www/html/** to both our containers, **nginx-container** and **php-container**. While this is perfectly legit, it is a common practice to have a special data container for this purpose. Data container holds data and all other containers are connected or linked to it.

In order to set this up, we need to change our **docker-compose.yml** file once again:

```
     nginx:    
      build: ./nginx/  
      container_name: nginx-container  
      ports:  
       - 80:80  
      links:  
       - php  
      volumes_from:  
       - app-data  
      
     php:    
      image: php:7.0-fpm  
      container_name: php-container  
      expose:  
       - 9000  
      volumes_from:  
       - app-data  
      
     app-data:    
      image: php:7.0-fpm  
      container_name: app-data-container  
      volumes:  
       - ./www/html/:/var/www/html/  
      command: “true” 
```

As you can see, we added a new container, **app-data-container**, which uses the parameters of the same volumes we used for **php-container** and **nginx-container** so far. This data container will hold the application code, so it doesn’t need to run. It only needs to exist to be accessible, but since it won’t serve any other purpose, there is no need to keep it running and thus wasting resources.

We use the same official image we already have pulled previously. Again, this is to save some disk space. We don’t need to pull any new image for this purpose, the php image will work just fine. Also, we told Docker to mount volumes from **app-data** for **nginx-container** and **php-container**, so we won’t need volumes options for those anymore and we can delete it.

Finally, we say that both **nginx-container** and **php-container** will use volumes from **app-data-container**. Run **docker-compose up -d** once again. As you can see in the terminal, Docker has just created a new **app-data-container** and re-created **php-container** and **nginx-container**.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*-Nk-Yg3tiR-E2ION.png)

Now, let’s see the list of containers, but this time, let’s display all containers, not just the those that are running:

```
     docker ps -a 
```

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*wLosT2--Mug0cU9B.png)

As you can see, the **app-data-container** has been created but it’s not running because there is no reason for it to run. It only holds data. And it has been created from the same image as **php-container**, so we saved hundreds of megabytes we would otherwise need if we pulled data-only container.

### MySQL

We need to modify our php image because we need to install the extension that will allow php to connect to mysql. To do so, we will create a new folder named **php** and inside we will create a new Dockerfile with this content:

```
    FROM php:7.0-fpm  
    RUN docker-php-ext-install pdo_mysql 
```

Your folder structure should look like this now:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*_7Iwd0Q3jBzZVIWE.png)

Next, we need to change our **docker-compose.yml** file again. We will change the way the **php-container** is built, next we will add **mysql-container** and **mysql-data-container** and finally, we will link **php-container** to **mysql-container**.

We will also define some environment variables for **mysql-container**. **MYSQL\_ROOT\_PASSWORD** and **MYSQL\_DATABASE** variables will be applied only if a volume doesn’t contain any data. Otherwise, these will be ignored. It makes sense because otherwise, we would create a new database with the same name and root password each time we would spin up a container, thus overwriting our database content. Not the behavior we want. I will name my database **zavrel\_db** but go ahead and change the name if you feel like it!

As with **app-data-container**, **mysql-data-container** will just hold the data, this time not our application code, though, but database data like tables with rows and their content. Since we won’t access this data directly, we don’t really care where they will be located on our host machine, so we don’t need to mount them to our directory structure.

```
     nginx:    
      build: ./nginx/  
      container_name: nginx-container  
      ports:  
       - 80:80  
      links:  
       - php  
      volumes_from:  
       - app-data  
      
     php:    
      build: ./php/  
      container_name: php-container  
      expose:  
       - 9000  
      links:  
       - mysql  
      volumes_from:  
       - app-data  
      
     app-data:    
      image: php:7.0-fpm  
      container_name: app-data-container  
      volumes:  
       - ./www/html/:/var/www/html/  
      command: “true”  
      
     mysql:    
      image: mysql:latest  
      container_name: mysql-container  
      volumes_from:  
       - mysql-data  
      environment:  
       MYSQL_ROOT_PASSWORD: secret  
       MYSQL_DATABASE: zavrel_db  
       MYSQL_USER: user  
       MYSQL_PASSWORD: password  
      
     mysql-data:    
      image: mysql:latest  
      container_name: mysql-data-container  
      volumes:  
       - /var/lib/mysql  
      command: "true" 
```

To test our MySQL setup, we will modify our **index.php** as well, so we can try to access our database:

```html
     <!DOCTYPE html>  
     <head>  
      <title>Hello World!</title>  
     </head>  
      
     <body>  
      <h1>Hello World!</h1>  
      <p><?php echo 'We are running PHP, version: ' . phpversion(); ?></p>  
      <?  
       $database ="zavrel_db";  
       $user = "user";  
       $password = "password";  
       $host = "mysql";  
      
       $connection = new PDO("mysql:host={$host};dbname={$database};charset=utf8", $user, $password);  
       $query = $connection->query("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_TYPE='BASE TABLE'");  
       $tables = $query->fetchAll(PDO::FETCH_COLUMN);  
      
       if (empty($tables)) {  
        echo "<p>There are no tables in database "{$database}".</p>";  
       } else {  
        echo "<p>Database "{$database}" has the following tables:</p>";  
        echo "<ul>";  
        foreach ($tables as $table) {  
         echo "<li>{$table}</li>";  
        }  
        echo "</ul>";  
       }  
      ?>  
    </body> 
```

This new script will take values we defined for the database, user, and password (notice that these are the same as environment values we set for our **mysql-container**) and try to establish the database connection. Once the connection is established, the script will try to select all tables from **INFORMATION\_SCHEMA** where table type is **BASE TABLE**. Now, if you’re not familiar with MySQL, this might be a bit confusing for you.

*NOTE: Basically, every MySQL instance has a special database that stores information about all the other databases that the MySQL server maintains. This special database is called* ***INFORMATION\_SCHEMA****.* ***INFORMATION\_SCHEMA*** *database contains several read-only tables. They are actually views, not databases. Databases are of* ***BASE TABLE*** *type.*

So when we try to select the table of type **BASE TABLE** we are actually looking for a database only and it is the database we will yet have to create. If it’s too much for you, don’t worry, it will all make sense soon.

Anyway, once you have **Dockerfile** and **index.php** updated, run **docker-compose up -d** again. Docker will pull mysql image, next, it will download and install a php extension for connection to the database.

Finally, it will start **app-data-container**, create **mysql-data-container** and **mysql-container** and recreate **php-container** and **nginx-container**.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*X6pt7ZF9toPl2iCs.png)

Check with **docker ps -a** that you have five containers now, 2 of them exited (**mysql-data-container** and **app-data-container**).

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*NN0xm5eNbN3MVn3I.png)

Refresh **index.php** in your web browser. You should see this line at the bottom: **There are no tables in the database “zavrel\_db”.**

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*4aeJvOEd9lEWdaGP.png)

Which is perfectly fine because we haven’t created any tables in our database yet. However, there are already some tables, but those are not visible by a regular user. If you want to see them, change **$user** to “root” and **$password** to “secret” in **index.php**. This way, you will get access to everything!

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*SeKnA2L9RTTAZ5BO.png)

Refresh the browser once more:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*7hK4a8z9C7DFtOce.png)

What a list! Right? Ok, let’s put back our regular user who can see only what he should see:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*3mvU9qVXTH1ARGye.png)

### Deep down the rabbit hole

So far, containers were like black boxes for us. We ran them, we listed them, but we never saw what is inside. That’s about to change now. I will show you how you can get right inside **mysql-container** and work with mysql server from within.

Run this command from your terminal:

```
     docker exec -it mysql-container /bin/bash 
```

Now you are inside the container! You can tell by the new prompt in your terminal:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*gesDU5MEqASAKb7n.png)

It consists now of **root@** followed by the ID of the **mysql-container**. In my case, it’s **root@8a56e15cdd4d**, in your case the ID would be different, but it is the same ID your **mysql-container** has assigned. Want to check? List all running containers by **docker ps** and look for the **CONTAINER ID** in the list, it’s the first column.

You can now take a look around as you would in any other Linux system:

*   **ls** command will show you the list of files and directories,
*   **pwd** command will print the current directory, which is root directory (/),
*   **uname -or** command will show you the kernel release and that this is actually a Linux operating system.

Remember, how we defined the volume for **mysql-container** in **docker-compose.yml** file?

```
     volumes:   
      - /var/lib/mysql 
```

Let’s take a look at this directory:

```
     cd /var/lib/mysql 
```

**ls** command will show you its content:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*C0PYfyrLvVPYY1df.png)

All right, let’s end this quick trip by going back to the root directory:

```
     cd / 
```

Now, we will run **mysql command-line interface** (MySQL CLI) inside our **mysql-container** that will allow us to work with the database server.

*NOTE: I want you to stop now for a while to let this sink and appreciate. You are working on your physical computer. This computer is running an operating system, Windows or Mac (if you’re on Linux, it’s a bit different). Inside your operating system, you are running a Docker container which is basically a Linux machine.   
 Now, we will go even deeper and run another command-line interface to work with the database server. Can you see how we go deeper and deeper, layer after layer, down the rabbit hole?* 🙂

Ok, let’s go back to work! To get access to MySQL CLI, we need a username and password. Luckily for us, we already created both user and password when we set up environment variables for our **mysql-container** in **docker-compose.yml** file. I hope you noticed that we also set up the root password as an environment variable. Remember this line?

```
     MYSQL_ROOT_PASSWORD: secret 
```

You might ask, how do we know that there is a user named root. Well, there is always this user. That’s why we were able to set the password for him with MYSQL\_ROOT\_PASSWORD variable without even questioning his existence.

To sign in mysql server, though, we won’t use root access because that would give us too many results as root can see everything.

Sign in with a regular user instead: **mysql -uuser -ppassword**

**\-uuser** means **user is “user”**

**\-ppassword** means **password is “password”**

Run the command and you will be taken deeper, inside the world mysql server. Again, you can tell by the prompt which changed now from **root@8a56e15cdd4d** (different ID in your case) to **mysql>** that we are somewhere else.

Inside mysql, there are different rules and different commands. Start with the command **show databases;** Don’t forget the semicolon! I told you, there are different rules in this world.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*7m-vILkJ7SzWMqcd.png)

You will see the nice table with the list of all databases available. One of them is our own database with **zavrel\_db**. Remember when we created it? Again, we defined it while preparing our **mysql-container** in **docker-compose.yml** file: **MYSQL\_DATABASE: zavrel\_db**

Let’s create a new table in our database. First, we need to select it, so mysql knows which database we want to work with:

```
     use zavrel_db 
```

You will get the information that the database has been changed. Now, we can create a new table:

```
     CREATE TABLE users (id int); 
```

Go to your web browser and refresh the page, you will see this table in the list:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*sFW-leWvnGDwPWgb.png)

Ok. We are done here, let’s get all the way back to the familiar terminal of our computer. First, we need to leave MySQL CLI. This can be done by command **q**

Go ahead and run it! MySQL will say **Bye** and you are back inside your **mysql-container**. Again, you can tell by the prompt **root@8a56e15cdd4d**. Let’s go one layer up. To leave **mysql-container**, just use the shortcut **CTRL + D** or type **exit** and hit enter. See? We are finally back to our computer terminal! How was it? Did you like the trip? I hope you did!

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*ZzVvaJIkrcIwmxHs.png)

I wanted to show you this rather complicated way of working with databases and tables so you can truly appreciate the web client we will learn about in a minute, but first, I want to go back to volumes once again, because we need to address few more things about them.

### Inspecting containers

Remember how I told you that we don’t really care about where Docker stores volumes of **mysql-data-container** on our computer (host machine) because we won’t access them directly anyway? Well, if you are curious where they are nevertheless, there is a way how to find out.

Run this command:

```
     docker inspect mysql-data-container 
```

Look for the **Mounts** section in the output you will get. Next to **Source** attribute is the location of database data on our host machine. It should be something like **/var/lib/docker/volumes/** and so on provided you are on Mac.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*cqkwO8LN5w7Hr7PM.png)

### Dangling volumes

When you create a container with mounted volumes and later destroy the container, mounted volumes won’t be destroyed with it unless you specifically say you want to destroy them as well. Such orphan volumes are called dangling volumes.

So far we used a command **docker rm container-name -f** to remove containers, but if you want to destroy volumes as well, you need to add another option, **\-v**. So it will look like this: **docker rm -v container-name -f**.

But what about containers we already destroyed so far without destroying their volumes as well? Let’s check out if there are any such volumes. First, let’s list all the volumes we have created so far:

```
     docker volume ls 
```

Now let’s narrow our list by adding the filter for dangling volumes only:

```
     docker volume ls -qf dangling=true 
```

**\-q** stand for quiet which only displays volume names

**\-f** stands for filter

It seems like we have some:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*XKp86jwL484i1JI4.png)

To delete them, we will combine two commands here:

```
     docker volume rm $(docker volume ls -qf dangling=true) 
```

This will remove all dangling volumes for us. Since Docker 1.13 you can use an easier command instead:

```
     docker volume prune 
```

This will remove all volumes not used by at least one container. Now if you check volumes again, you should have only one volume left:

```
     docker volume ls 
```

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*YoGQHg2_3bdhA2gY.png)

We reclaimed almost 500 MB of space!

### phpMyAdmin

Ok, let’s move on and spin up our last container. phpMyAdmin is a great tool for managing mysql databases directly from the web browser. No one will force you to stop your trips deep inside MySQL CLI if that’s what you like, but a web interface is way more convenient in my opinion. Add the following lines at the end of your **docker-compose.yml** file:

```
     phpmyadmin:    
      image: phpmyadmin/phpmyadmin  
      container_name: phpmyadmin-container  
      ports:  
       - 8080:80  
      links:  
       - mysql  
      environment:  
       PMA_HOST: mysql 
```

By now, everything should be fairly clear. We start with the official docker image, publish container’s port 80 to port 8080 of our host machine, so we can access phpMyAdmin from the web browser. We need to use a different port, though, because port 80 is already taken by the Nginx web server. Finally, we will link this container to our **mysql-container** and set an environment variable.

Go ahead and run this command once again:

```
     docker-compose up -d 
```

Docker will pull the phpMyAdmin image and create **phpmyadmin-container**.

Go to your web browser and type **:8080** behind the IP address your Nginx is working on. In my case, it looks like this **0.0.0.0:8080**, but **localhost:8080** works as well.

You should be presented with this login screen:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*IgKFbujtATWJYky2.png)

Now, log in as a regular user (user/password). You’re in mysql server! Check the list of databases on the left pane and click on **zavrel\_db**. Can you see the table **users** we have recently created inside MySQL CLI?

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*gcyUkcyYCz5SgnJy.png)

Give yourself a little break, maybe a cup of coffee, and let it all digest a bit. We will continue with more exciting stuff. But since now you have learned a lot! Pat yourself on your back for this!

### GitHub Volume

Mounting a local directory to make it accessible for **nginx-container** and **php-container** is fine until you want to deploy your application to some remote VPS (virtual private server). In such a case, it would be great to have your code copied to a remote volume automatically. In this section, I will show you how to use GitHub for this.

Let’s make a copy of our **docker-compose.yml** file and save it as **docker-compose-github.yml**. We will make some changes to our **app-data-container** so it won’t mount a local directory but rather get a repository from GitHub. In case you have your code on GitHub in a public repository, this will make it very easy to spin up your development environment on a remote server with the code cloned from your repository.

First, we need to create a Dockerfile for **app-data** image. Create a new folder called **app-data** and save the Dockerfile there with this content:

```
    FROM php:7.0-fpm    
    RUN apt-get update && apt-get install -y git    
    RUN git clone [https://github.com/zavrelj/docker-tutorial/](https://github.com/zavrelj/docker-tutorial/) /var/www/html/    
    VOLUME ["/var/www/html/"]
``` 

Your folder structure should look like this now:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*8ZDK5FwMAD_Ix_uE.png)

Again, we are using already pulled official php image, but on top of that, we will update the underlying **debian:jessie** Linux distro and then install **git**. Next, we will clone my public repository I have created for this purpose and save it inside **/var/www/html** directory inside our container. Finally, we will create a volume from this directory, so other containers, namely **nginx-container** and **php-container** can access it.

Now, we need to change **app-data** image instructions in our **docker-compose-github.yml** file like this:

```
     app-data:    
      build: ./app-data/  
      container_name: app-data-container  
      command: “true” 
```

Ok, let’s clean up everything, so we can start with a clean slate.

Stop all containers created with a **docker-compose** command:

```
     docker-compose stop 
```

Remove all those stopped containers including volumes that were attached to them:

```
     docker-compose rm -v 
```

Clean dangling volumes:

```
     docker volume prune 
```

In order to use our new **docker-compose-github.yml** file, we need to tell **docker-compose** about it, otherwise, it would use the default docker-compose.yml as always.

Rebuild the images with the new configuration file:

```
     docker-compose -f docker-compose-github.yml build 
```

and spin up containers again:

```
     docker-compose -f docker-compose-github.yml up -d 
```

Navigate to your page in the web browser and you should see this:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*h2gkx1mdI5kpTWH9.png)

### Digital Ocean

Let’s provision our development environment to a remote server. Digital Ocean is a great service. If you don’t have an account yet, sign up with [***my referral link***](https://m.do.co/c/e1324db90e6f) and you will get $10 in credit!

Once you’re in, create a new Droplet:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*wDhZh6zEDGK6Z2p4.png)

and choose Docker from **One-click apps**:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*SQjwvNpa80s6F_nb.png)

Pick the smallest size available, it’s more than enough for our purposes:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*Wu6KAv4IzDX35Q6f.png)

Since I want you to use SSH for the remote access to your Droplet, you need to set it up, unless you already have it. The whole process is quite easy. Open new terminal window and type:

```
     ssh-keygen -t rsa 
```

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*ik-mT_ut9GpJzydO.png)

When you’re asked where to save the key, just hit Enter.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*TuUXrZzFEvpvizvW.png)

If some other key is already there, it will be overwritten.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*0P1TgT6FPYOBjSiS.png)

Enter the password for the newly generated key (twice).

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*6EDpIucwuUTOE4OP.png)

Once you see this, your key is ready:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*v_FCvpqLenouU__s.png)

Run this command to display the public key, select it and use CMD + C shortcut to copy it to the clipboard:

```
     cat ~/.ssh/id_rsa.pub 
```

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*3cxlB28RY_k30Bhq.png)

Go back to Droplet setup and hit **New SSH Key** button:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*hYcOcidal3fYc2Ku.png)

Paste your copied public key to the from and fill the name of your computer:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*xrKbaXJXygQUXmrz.png)

Make sure, your computer is selected for SSH access and choose a hostname. Finally, hit that green button **Create**.

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*n-B3HNILFxjPw1Je.png)

Once your Droplet is created, write down its IP address.

### Transferring the project folder

If you have followed me step by step, you should have your **docker-nginx-php7** folder on your Desktop.

We will copy this folder to our Droplet so we can run Docker Compose with our YML configuration file remotely from the Droplet.

To copy the folder, we will use **rsync** command. Make sure you write this down exactly as it is. Instead of **IP**, use the actual IP address of your Droplet. We want to transfer the actual directory, not just the content inside it, so we need to omit the trailing slash:

```
     rsync -r -e ssh ~/Desktop/docker-nginx-php7 root@IP:~/ 
```

This command will ask for your SSH key password and then create a copy of **docker-nginx-php7** folder inside the home folder of the user root (**/root**).

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*9TItLTeJRw4VswpT.png)

Now, let’s check if everything has been transferred. SSH into your remote server (your actual IP address instead of **IP**):

```
     ssh root@IP 
     cd docker-nginx-php7 
     ls 
```

Can you see your familiar directory structure including two configuration files?

Nice! Everything seems to be in place!

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*18zeYeIUKKGa4lIs.png)

There’s no Docker Compose on this particular Droplet, but it’s fairly easy to install it. First, we need to install **python-pip**:

```
     apt-get update 
     apt-get -y install python-pip 
```

Next, we can install Docker Compose via pip:

```
     pip install docker-compose 
```

We are ready now to let Docker Compose do its magic. Let’s run our familiar command that will automate the whole process of pulling and building images, getting the code from GitHub and spinning up all containers. Since there are no images to rebuild, we can use the **up** command directly:

```
     docker-compose -f docker-compose-github.yml up -d 
```

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*8iYv5G4GPKOQt_AU.png)

Once everything is done and all containers are running, you can navigate to IP address of your Droplet ([**http://104.236.209.37/**](http://104.236.209.37/) in my case). Octocat should be waiting for you:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*tzW6StqGE7TMDcZ9.png)

And if you add port 8080 behind the IP address, you will get phpMyAdmin welcome screen:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*U6RtlVeGHifMnYWz.png)

Go ahead and login with user / password or root / secret, both will work. Make sure that our **zavrel\_db** database is there:

![](https://www.zavrel.net/images/Docker-and-Docker-Compose-for-PHP-development-with-GitHub-and-Digital-Ocean-deployment/0*xZAL9-ptpzHDZoOp.png)

**One last thing. Once you’re done with Digital Ocean, make sure to destroy your running Droplet so you won’t be billed. Or in case you used my referral link and received those $10 in credit, to not waste it all by running the Droplet you don’t need after you finish this tutorial.**

Alright! That’s all. I hope you have learned something useful today. If you liked this article, consider by complete web development course where I will show you how to use Docker in the development process of the whole discussion server based on PHP and MySQL. Learn more at [www.twdc.online](https://www.twdc.online).

## Want to learn more?

The rest of this tutorial is available for free on <a href="https://skl.sh/3eP78uy" target="_blank">Skillshare</a> as a part of the much larger and far more detailed video course. Again, if you're new to Skillshare, you'll also get premium access to more than 22.000 courses. Remember, that you won't be charged anything for your first 2 months and you can cancel your membership whenever you want within that period. Skillshare is Netflix for life-long learners.

<a href="https://skl.sh/3eP78uy" target="_blank">![](https://codewithjan.com/images/posts/twdc-skillshare-cover-play.png)</a>