Prologue
So, this is my first blog post/article on DevOps with PHP. Hopefully this will become a series of best DevOps practices in the PHP world and you and me will be able to follow my previous experience and future journey in exploring the vast ecosystem of technologies that are used to deploy your PHP applications to the cloud (and on premises). You can use it to get some info, I can use it to remind myself of what the hell I did before. I say it's a win-win situation, so let's get started.
Why dockerize PHP applications? (tl;dr? It's good and useful!)
So since you ended up on this end of the internet, I'm gonna guess you already know the why and wanna know the how. So without further a do, let's move on.
Nah, I'm not letting you skip this easily, but I won't do the regular cliche definitions of why dockerize PHP applications (and any others for that mater). I'll do my own retrospective, but feel free to skip to next section if you want to.
I'm a web developer since approximately 2008-2009 when I did tiny projects and worked for a company that worked outsourcing for other companies all over the world.
Back then PHP was progressing from PHP 4 and moving to more safer PHP 5 and then had a standardization boost with PHP 5.1. We used to work on many projects at same time, finished minor or bigger features for small or large projects and we just hopped from project to project. Similar happened later since 2011 when I decided (poorly in the first few years I might add...) to start my own freelancing gig, but this time it got more stable and hopping through projects was done maybe once or twice a year.
Later I started working for Asian companies, I started building their main projects (with their internal and external teams) and did a lot of moving through projects and found maintaining service versions in environment was a challenge (Docker containers came to save me).
What were the issues you might ask? Well, PHP versions or database engines for example, would be different on each project, depending on the time it was conceived and started, versions would always be different. In one project you would have PHP 7.0 with Postgres 11, on another you would have PHP 7.1 with Postgres 13. Then maybe I would have Redis 5 and 6 and then I would lose the rest of the hair on my head....
Keeping all these different versions all installed directly on a developer's PC and making sure it's the same across other people in the team was a nightmare. We would constantly have different people use different environments. Windows people would use the Windows builds of PHP and other services and they always had some specific thing that "worked on their machine", but randomly failed on an actual staging or production server (different path delimiters \
vs /
). Same would be for Mac or Linux users, they would always have some different version of the service (PHP for example) installed in the beginning and they wouldn't bother to upgrade much since it would break previous older projects. All of these would either use deprecated or differently build functions that didn't work on main servers or other environments. Other extreme was if it was their fresh install with latest PHP, they'd use some newly introduced functionality that wasn't available on the staging or production servers and that would fail.
Not to mention additional problem with app configuration, you update on your local but forget to update on staging and production or you forgot to tell someone to update. Or you told them, they listened and then they forgot to update. Needless to say, many many issues rose from this discrepancy between all these environments.
So if you've worked so far like this on multiple projects, you would know the pain from these things I've mentioned so far. Dockerizing makes all the environments (local, staging, production and so on) all the same! You use the same config as it was committed in code, you don't tell someone to do it. You do it, someone reviews it, it's getting built or pulled and without too much stress it's same everywhere. It's helping something called Infrastructure as Code (IaC) and tbh, I love it, makes me forget stuff and find it written and not wonder how it was done. If I need to deploy it to production, I just build an image there or build it in another place and just pull my image. Many possibilities. Not to mention Kubernetes and serverless stuff.
There are still some platform issues for Docker on Windows and Mac (mostly performance issues and some CPU connected issues between x86 and Apple's ARM chips) but it's all worked on by maintainers of packages and OSS all over the world, so the situation is getting better (Windows now uses WSL which makes containers much faster than in the past).
How to dockerize a PHP application
We're gonna start simple. We'll have a PHP application with only a single page. For simplicity, we'll set the content in a src
subfolder and call the file index.php
with the next content:
<?php
echo "Hello World from a Dockerized PHP Application";
Now, as with almost all Docker installations we need to create a Dockerfile and build our dockerized PHP application.
The content of the Dockerfile is simple, first we tell it from what image we will start building our own Docker image. In our case we want the official PHP 8.2 image that can be found on Docker Hub. For simplicity, we're going to use the Apache image they provide since it will serve the page.
FROM php:8.2-apache
Great, we can check if this works and build our own Docker image from the project root
docker build -t dockerizing-php:0.1-apache .
In our case we used -t
tag parameter to name and tag the image, and we used similar naming convention so that we know easily from where we derived the image from.
As you see, if you tried too, it finished the build successfully, so good start!
Then we can start our own image that contains Apache server with PHP by using this command:
docker run -p 8000:80 dockerizing-php:0.1-apache
and then visit http://localhost:8000/
Oh, nice, it works, but kinda doesn't. We see a default 403 Apache error instead of the Hello World message. This means that we forgot to include our own code somehow in the image and it's not in the running container. There are two ways to include stuff in your images and running containers and here we'll cover and differentiate both.
Build time copying
First way of including our files into Docker images is by directly copying them during build step. It's just a simple copy operation from our Host OS (the actual computer we work on) and the Docker image that we are building. The Apache PHP image by default works with Document root in the /var/www/html
folder, so we need to copy our files into that folder. Let's copy and paste our next command into the Dockerfile
COPY ./src /var/www/html
Then we stop previous docker run command (usually by using Ctrl+C) and then try again both the build and run commands
docker build -t dockerizing-php:0.1-apache .
docker run -p 8000:80 dockerizing-php:0.1-apache
Then again visit http://localhost:8000/
and see the glory of a Hello World PHP application!
Awesome, it worked! Good job on your first PHP Docker container!
Now, this is great for building and deploying an app onto servers that have Docker installed since it would contain the code inside the image. But if you try to change your code and refresh the page, that change will never show up in the browser until the next time you do docker build and run again the application. The code was copied only at build time and any change in your project after Docker building will not be shown. This can be solved by using Docker Volumes.
Volumes loading
So what are Docker Volumes? I simply understand them as shared folders on the PC, the "only" difference being that both things are local on your PC. So when you share a folder from your PC as a volume, you can create or overwrite an internal path that doesn't exist or exists in the image. Volume can be folder or file, as you wish, I'll touch that in next blog post
The changes in the folder that you have on your local/host machine, will be immediately available in the running Docker container, which works perfectly with PHP development.
So since volumes work only when running the Docker container all we need to do is update our previous docker run command to this
docker run -p 8000:80 -v ./src:/var/www/html dockerizing-php:0.1-apache
and you will see on http://localhost:8000/
that it runs fine.
While it runs, you can try to edit your PHP code and it will update immediately after page refresh.
Great work, now you have a working local development environment and you haven't installed any PHP version manually. It's all contained in the Docker containers.
Summary
You've seen how to dockerize any PHP application. You've seen why if you didn't skip that part. You've seen where you can use it and how. I hope I've helped in explaining the general why and how on dockerizing PHP applications. Once you start using it for all your PHP projects, for all clients, for many microservices, you will realize that it's a crucial tool for standardizing and streamlining your and your team development workflows. Not to mention that you will reduce the failure on staging/production and other environments due to "it worked on my PC" issues. Also, less headaches in general!!
In my next blog posts I will show and explain a more robust version of dockerizing our PHP application and give some examples how it can be used to have many different projects running independently with no interference between each other. First I will try to cover using docker compose and building whole project with it, doing the whole workflow with docker compose and the pros and cons I have seen over the years. Till then, best regards from me!
Source code for this tutorial/post, can be found here: https://github.com/goran-despotoski/1-dockerizing-php-app