Setup a basic Local PHP Development Environment in Docker
Tags: docker, php, devops, beginners
Published: 2020-06-27 00:29:48

<!-- wp:paragraph -->
<p>In this post we'll learn how to setup a basic local PHP development environment in Docker. We'll be using docker-compose and Dockerfile to achieve this. We'll use PHP, Apache, and Mysql. Setting up a comprehensive environment with things like task runners, composer, etc... will come in later tutorials.</p>
<!-- /wp:paragraph -->

[Learn to code with our beginner friendly tutorials](https://truthseekers.io/latest-tutorials-signup/)

<!-- wp:paragraph -->
<p>If you just want the quick and dirty source code, then skip to the bottom. We're going to take this nice and slowly going step-by-step.</p>
<!-- /wp:paragraph -->

<!-- wp:block {"ref":3836} /-->

<!-- wp:paragraph -->
<p><strong>Pre-Requisites: Docker &amp; Docker-compose installed.</strong></p>
<!-- /wp:paragraph -->

<!-- wp:core-embed/youtube {"url":"https://www.youtube.com/watch?v=ThpnqYpvnIM\u0026feature=youtu.be","type":"video","providerNameSlug":"youtube","className":"wp-embed-aspect-16-9 wp-has-aspect-ratio"} -->
<figure class="wp-block-embed-youtube wp-block-embed is-type-video is-provider-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">

</div></figure>
<!-- /wp:core-embed/youtube -->
{% youtube ThpnqYpvnIM %}
<!-- wp:block {"ref":4009} /-->

<!-- wp:paragraph -->
<p><strong>First step: Get a simple PHP script to run with Dockerfile</strong>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>If you take a look at the <a rel="noreferrer noopener" href="https://hub.docker.com/_/php" target="_blank">PHP Docker docs</a> you won't see anything about a docker-compose file, but we can figure out how to do it with what they do give us. At the very beginning they give us a simple Dockerfile setup. Let's use that for our Dockerfile in the root of our project directory: I'm calling my project "php-docker". Then inside make a <code>Dockerfile</code> with this code:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>FROM php:7.4-cli
COPY . /usr/src/myapp
WORKDIR /usr/src/myapp
CMD &#91; "php", "./index.php" ]</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>This code is the setup to create the image we'll use.  This setup pulls in PHP 7.4 with some command line stuff.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The <code>COPY . /usr/src/myapp</code> copies the contents of the current directory into /usr/src/myapp inside the Docker container. The next line: <code>WORKDIR /usr/src/myapp</code> sets the /usr/src/myapp as the "working directory", kind of like you would cd /to/your/project.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Then from inside the /usr/src/myapp folder we run the command <code>$ php ./index.php</code> where index.php is our script. So that means we need a PHP script. Let's create our <code>index.php</code> file inside the project directory like this:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>&lt;?php

echo "Hello from the docker container";</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now we can continue with their instructions, which is to build and run the docker image:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>$ docker build -t my-php-app .
$ docker run -it --rm --name my-running-app my-php-app</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>The first command is going to build the image with the name "my-php-app" on your computer using the contents of the current directory. The second command prepares a container called "my-running-app based off the image we just created "my-php-app". We setup to run the index.php script, so our script runs in the command line. We should get something like this:</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":4157,"sizeSlug":"full"} -->
<figure class="wp-block-image size-full"><img src="https://truthseekers.io/wp-content/uploads/2020/06/image-20.png" alt="" class="wp-image-4157"/></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>First step accomplished! But simple PHP scripts aren't very useful. Let's set this up on an Apache web server. If you scroll further down the documentation you'll see an "Image variants" section with one of them being php-apache. This image has Apache bundled with PHP. This will allow us to easily get our script running on a webserver and display the script results in a browser. Again, let's follow the directions. <strong>To run the Apache/php image WITHOUT a dockerfile, run this command:</strong>  (Note the first line is for Linux/Mac users and the 2nd line is for the lonely Windows users</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>docker run -d -p 80:80 --name my-apache-php-app -v "$PWD":/var/www/html php:7.2-apache # This line for *nix users
docker run -d -p 80:80 --name my-apache-php-app -v C:\Users\fastp\Desktop\Code\tutorials\php-docker:/var/www/html php:7.2-apache   # For Windows users</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>This line is basically saying RUN the container and set the name as "my-apache-php-app.  -p is to set the PORT mapping from our local machine to the port on the container. the left side is our local port, the right side is the port on the container.  Then use -v to set the VOLUME, or... basically bind our present working directory to the /var/www/html folder. This essentially puts the contents of our current directory into the html directory on the container so our code can run inside it. At the end we define the image we build from, which is<strong> php:7.2-apache</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Note that Windows doesn't have a $PWD command, so I had to manually put my path for this to work.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Okay, so we setup to have our script run on port 80, so go ahead and visit <a rel="noreferrer noopener" href="http://localhost:80" target="_blank">http://localhost:80</a> and you should see your script running in the browser.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":4158,"sizeSlug":"full"} -->
<figure class="wp-block-image size-full"><img src="https://truthseekers.io/wp-content/uploads/2020/06/image-21.png" alt="" class="wp-image-4158"/></figure>
<!-- /wp:image -->

<!-- wp:heading {"level":3} -->
<h3>PHP and docker-compose</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Now let's start moving stuff into a docker-compose file, but first let's stop that container we just created. You can run a <code>docker ps</code> to get the container id and then run <code>$ docker stop container_id</code> but I'm just going to stop all containers like this:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>docker stop $(docker ps -a -q)</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now create a <code>docker-compose.yml</code> file in the root of your project and put the following inside:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>version: '3.1'

services:
  php:
    image: php:7.4-apache
    ports:
      - 80:80
    volumes:
      - ./src:/var/www/html/</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>The top line just sets the version number of docker-compose we're using. Then we have the "services" which is the list of containers to setup. We'll be giving this container the name of "php" so other containers can connect via that name. The "image" will be php:7.4-apache  (even though our previous command line experiment used 7.2)</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Then just like in our command line trial we're setting up the PORT mapping from 80 on our local machine to 80 on the container. Finally we'll stuff the contents of our ./src folder into the containers <code>/var/www/html</code> folder.  Now let's <strong>move our script into the ./src folder</strong> on our local machine. My project structure now looks like this:</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":4160,"sizeSlug":"full"} -->
<figure class="wp-block-image size-full"><img src="https://truthseekers.io/wp-content/uploads/2020/06/image-22.png" alt="" class="wp-image-4160"/></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Now run <code>docker-compose up -d</code> from inside the project root and visit <a rel="noreferrer noopener" href="http://localhost:80" target="_blank">localhost:80</a> and see the script running n the browser. Because we're using volumes to stick our code in the container we should be able to change the script and have it update automatically. Give that a try.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Now let's shut down our container with <code>docker-compose down</code>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The next step is to setup <a rel="noreferrer noopener" href="https://hub.docker.com/_/mysql" target="_blank">MySQL and Adminer (a DB access tool)</a>  Luckily their docs give us a docker-compose example so we don't have to think as much. We do need to modify the settings a tiny bit though because to connect PHP to MySQL we need to install a few missing pieces to our PHP environment. Here's what our <code>docker-compose.yml</code> file should look like now:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code># Use root/example user/password credentials
version: '3.1'

services:
  php:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 80:80
    volumes:
      - ./src:/var/www/html/

  db:
    image: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example

  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Our PHP service looks different because to connect PHP to the database using mysqli we need to install some mysqli stuff. That means we need to use a Dockerfile to customize our php7.4-apache image. So now we're using "build" to use the contents of the current directory (the dot) and using our Dockerfile to create the image. (Dockerfile image code below)</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>So now we've added a "db" service which is the mysql image. They use some weird command that has something to do with the password :-) and a restart policy. They also setup an environment variable which sets the root password to "example", so that's how we'll log in.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Then adminer gets the adminer image and sets that port mapping to 8080:8080.  Just like in XAMPP how you go to localhost/phpmyadmin to get the database management tool, well... with this you go to localhost:8080 to get to the adminer db tool.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Let's also setup Mysqli to work inside our PHP container. Here's my <strong>Dockerfile code:</strong></p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>FROM php:7.4-apache
RUN docker-php-ext-install mysqli</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>I know that's a lot of code to process.... So let's take it line-by-line. The first line uses the php:7.4-apache image to build the container. The second line runs a command to install the mysqli extension inside the container. From here we have everything we need to have a super basic PHP development environment. So let's run <code>docker-compose up -d</code> again.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Now we should be able access Adminer and log in. I'll just use a few images to speed things up. Just note that the name of the "server" is "db" in our case based on the dockerfile. We could have named it "pie" or anything we wanted.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":4162,"sizeSlug":"full"} -->
<figure class="wp-block-image size-full"><img src="https://truthseekers.io/wp-content/uploads/2020/06/image-23.png" alt="" class="wp-image-4162"/></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Select "Create Database"</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":4164,"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="https://truthseekers.io/wp-content/uploads/2020/06/image-25.png" alt="" class="wp-image-4164"/></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Name it and hit save. I named mine "company1"</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":4165,"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="https://truthseekers.io/wp-content/uploads/2020/06/image-26.png" alt="" class="wp-image-4165"/></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Then select "new item" to add a few items to your table.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":4167,"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="https://truthseekers.io/wp-content/uploads/2020/06/image-27.png" alt="" class="wp-image-4167"/></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Now we just need the PHP to connect to our database. This isn't a PHP tutorial so I won't explain it all, but it just connects to our database and runs some queries and prints them on the screen. The important part is the third line that connects MySQL to PHP. </p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>&lt;?php

echo "Hello from the docker yooooo container";

$mysqli = new mysqli("db", "root", "example", "company1");

$sql = "INSERT INTO users (name, fav_color) VALUES('Lil Sneazy', 'Yellow')";
$result = $mysqli->query($sql);
$sql = "INSERT INTO users (name, fav_color) VALUES('Nick Jonas', 'Brown')";
$result = $mysqli->query($sql);
$sql = "INSERT INTO users (name, fav_color) VALUES('Maroon 5', 'Maroon')";
$result = $mysqli->query($sql);
$sql = "INSERT INTO users (name, fav_color) VALUES('Tommy Baker', '043A2B')";
$result = $mysqli->query($sql);


$sql = 'SELECT * FROM users';

if ($result = $mysqli->query($sql)) {
    while ($data = $result->fetch_object()) {
        $users&#91;] = $data;
    }
}

foreach ($users as $user) {
    echo "&lt;br>";
    echo $user->name . " " . $user->fav_color;
    echo "&lt;br>";
}</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now here is the result of our code when we run it in the browser. :</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":4169,"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="https://truthseekers.io/wp-content/uploads/2020/06/image-28.png" alt="" class="wp-image-4169"/></figure>
<!-- /wp:image -->

<!-- wp:heading {"level":3} -->
<h3>Persistent Mysql using Volumes: </h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>You're probably not going to build your entire app in one sitting so we'll want to use volumes for persistent data like the stuff inside your database. <strong>Without a volume your data will be erased every time you stop the containers.</strong> In order to do that you just need to add a few more lines to your code. The code from db: and down will look like this:  (The important lines are at the end of the "db" section where it says volumes, and the VERY end where it says "volumes"</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>  db:
    image: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
    volumes:
      - mysql-data:/var/lib/mysql

  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080

volumes:
  mysql-data:
</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Note the indentation. <strong>volumes will be at the same indentation level as "services"</strong>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>I won't go deep into volumes right now. (To be honest I'm no expert yet lol) but in this code we're setting up a volume on our system (managed by docker) called mysql-data. All our database tables and junk gets stored inside the "mysql-data".  Then we map that data to /var/lib/mysql on the container.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>So that's pretty much it y'all. If you want to get in some practice you should try to swap out MySQL for Postgres. Or Apache for Nginx, or try to install some other tools like Composer using the Dockerfile. Please let me know if the article helped or not. Would love some feedback so I can make these better. </p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4><a rel="noreferrer noopener" href="https://github.com/truthseekers/php-docker-simple" target="_blank">Here's the source code </a></h4>
<!-- /wp:heading -->

[Learn to code with our beginner friendly tutorials](https://truthseekers.io/latest-tutorials-signup/)