In this article, I’ll lay out how to best get started with setting up a PHP dev environment, walking you through how to get set up with Docker. There are so many ways to set up your PHP dev environment, but using Docker is the current best practice.
I’ll start with a brief history of how people have set up their PHP dev environment over the years, leading up to where we are now. But if you’d rather skip all that and just get your server running, you can skip right to the configuration steps.
A Little Background
One of the problems with web development is that things change at a rapid pace. CSS best practices change as new properties are added to the specification. (How did we ever cope without CSS Grid?) PHP is now on version 8, and even the tools we use to execute PHP scripts are being refined over time. As a result, a lot of the tutorials get outdated quite quickly.
Until a couple of years ago, I sent everyone I was teaching to Bruno Skvorc’s excellent article Re-introducing Vagrant: The Right Way to Start with PHP. At the time it was a fantastic introduction to the (then) best way to set up a local development environment.
That article is only from 2015, but five or six years is an eon in ever-moving web development timescales. The “right way” has moved on quite significantly since then.
I’ll quickly recap how things have changed over the years.
1. Manually installing PHP, MySQL and Apache
If, like me, you’re old enough to have been developing websites in the 90s, you’ll remember how frustrating the experience was. Back then, if you were in the minority who didn’t just develop on the live web server (yes, we really did this, yes it was a terrible idea), you would manually install Apache, PHP and MySQL on your development machine.
Getting a development environment set up required significant expertise. You needed to know how to configure the web server, how to configure PHP, and you had to go through the process of manually installing and configuring all the software you used. This was a time-consuming and daunting task in its own right for novice developers.
2. Preconfigured Packages such as XAMPP
By the early to mid 2000s, people had started putting together all the required software in a single package which installed and configured all the software you needed. These packages were things like XAMPP and WAMP, and at the click of a button they gave you a usable development environment.
If you hang around various PHP facebook groups, you’ll find that a significant portion of new developers still follow tutorials from this era and a large number of existing developers never moved on, so XAMPP is still used quite widely. If this describes you, it’s time to move on.
Using XAMPP made it very easy to get a web development environment up and running on your machine. Bruno’s article outlines the problems with this approach, but the main issue comes when you want to put your site live on the Web. The versions of PHP, MySQL and Apache (or NGINX) may be different from the ones you installed as part of your XAMPP package. In addition, there are a couple of minor, but frustrating, differences between Windows and Linux. If you’re developing your site on a Windows machine and uploading it to a Linux server, some of your code may not work at all once it’s uploaded.
3. Virtual Machines and Vagrant
In the late 2000s and early 2010s, the trend among developers was to move to a virtual machine. The idea was that you could run a copy of the real web server’s operating system with all its installed programs — the exact same configuration and setup as the actual web server you were going to eventually deploy your website to. That way, when you made the website live, there was no chance of it not working.
While many programmers saw the benefit of such an environment, the difficulty and time required to set this up meant that few did. That was until Vagrant (and associated tools like Puphpet) came along and took all the hassle out of doing so.
Take a look at the article I linked to earlier for an excellent description of Vagrant, Virtual Machines and the benefits of setting up a development environment in this way.
All this background brings us to today and the reason for this article. If Vagrant is so great, why use something else instead?
The main benefits of a virtual environment set up using Vagrant are:
Your development PC is not tied into a particular environment. You can host multiple websites: one using Apache, one using NGINX, one using PHP 7 and one using PHP 8.
When the site is made live, the website is being uploaded to exactly the same environment that it was developed on.
It’s easy to see why developers want this. Taking the next step up to Docker keeps these benefits while avoiding some of the drawbacks of Vagrant/Virtual Machine environments.
What’s wrong with Vagrant?
Despite the benefits, a Vagrant-based development environment introduces its own set of restrictions and problems.
System resources. Vagrant requires running a whole different operating system. You need to download and install the operating system that’s running on your web server, and all the packages it has installed. This uses significant amount of disk space and memory. A virtual machine will normally need at least 512 MB RAM. That’s not a lot for today’s computers, but it quickly adds up. If you want to host one website on PHP 7 and one on PHP 8, you need two different virtual machine instances installed and configured on your computer.
You have to ensure the virtual machine and the server are in sync. Whenever you update the server or change the server’s configuration, you have to remember to update your local development environment with the same changes.
It locks you tightly into a server OS and configuration. Moving a website from one server to another is a difficult task. A website is more than just the PHP scripts, images and CSS that make it up. A specific server configuration (such as installed PHP extensions and
httpd.conf) are also required for the website to function correctly.
There is a very limited choice of available packages. Depending on which Linux distribution your web server is running, you may not have any choice over which version of PHP you run. Unless you install packages from third-party repositories, you won’t be able to use the latest and greatest PHP version. At the time of writing, PHP 8 has recently become available. If you’re using CentOS 8/RHEL 8, you’re stuck with PHP 7.3 until you get a new version of the operating system. If you’re on Debian, the latest version available is 7.3. Other distributions will have different versions available.
The server configuration is global. PHP has a settings file called
php.ini. Changing this applies the updated configuration to every website hosted on the server. The same goes for
nginx.conffor NGINX or
httpd.conffor Apache. The MySQL database instance has databases for all sites hosted on the server. Making any large-scale database configuration changes is far reaching. Updating a MySQL setting will affect every website using that MySQL server!
The package versions are global on the real server. Although it’s possible to run multiple PHP versions on the same web server, it’s difficult to configure and can have bizarre side effects depending on what your script is doing (such as when you have a script you want to run in a systemd unit/cronjob and forget that you should be using
Although points 5 and 6 can be overcome on the development machine by running different Vagrant virtual machines, you’ll need a real web server that mirrors each configuration you’re running so that the websites work when you upload them.
Docker solves all the problems listed above. But just what is Docker and how does it work?
Let’s start with the intro from Wikipedia:
Docker is a set of platform as a service (PaaS) products that use OS-level virtualization to deliver software in packages called containers. Containers are isolated from one another and bundle their own software, libraries and configuration files; they can communicate with each other through well-defined channels.
Before getting too technical, the practical benefit to us as web developers is that Docker allows us to package up everything the website needs, all the PHP code along with the PHP executable, MySQL server and NGINX server in addition to the configuration files used by those programs.
All the website’s code, and the exact versions of the programs needed to run that code, are packaged together, effectively as a single application. This entire application can then be run on any operating system. When someone runs the packaged application, PHP, MySQL, NGINX and all the PHP files you wrote are all embedded in the application itself. Even better, the exact MySQL, NGINX and PHP versions are part of the package. When you run the application, the exact versions of these tools that the application was developed for are downloaded and installed.
“Isn’t that what a virtual machine already does?” I hear you ask. Yes it is, but there’s a big difference between the way Vagrant and Docker handle software installs.
With Vagrant, running a Virtual Machine, the complete operating system with a specific PHP version, MySQL version and (usually) server configuration is cloned from the real web server. When the server is updated, the virtual machine must also be updated.
When using Docker, however, the PHP/MySQL/NGINX version is provided as a single package known as an image, and the server can run as many different images as you like.
The benefit here is that the web server and your development machine are both running the exact same image. You just upload your image to the web server, run the entire application there and your website is up without needing any web server configuration at all.
Additionally, each image is entirely separate from other image on the server. Each image (one per website in this simplified example) is separate from each other. Each website will have its own NGINX configuration, its own
php.ini and its own installs of PHP and MySQL. Each website can be running entirely different PHP versions. You can even have one website running on Apache and one website running on NGINX, on the same machine at the same time. Even when you’re running two different NGINX websites, you’ll have two different NGINX processes, with their own configurations, running at the same time.
This has a small memory overhead, but the flexibility it grants makes this a very worthwhile trade-off:
The entire website, with the required PHP/MySQL versions, all the configuration and all the code can be moved around with ease. Moving the website to a new server requires just copying a single folder. You don’t need to make any changes to the PHP or NGINX configuration on the new server. You don’t even need to install PHP or NGINX on the server itself. They’ll be automatically installed by Docker when you launch the application.
You can run the exact same image on your development machine. Using Vagrant, you’re effectively running a copy of the server’s configuration/installed packages on the same machine. With Docker, the same exact same image is run on your PC for development as is being run on the server.
nginx.confconfiguration changes or updating PHP to the latest version is treated the same way as uploading updated PHP code to the server. You update the application, and it doesn’t matter whether that’s changing some PHP code or updating
Each image is self-contained in something called a “container”. A PHP script running in one image can’t access files running in another. Think
open_basedirbut much stricter. A container is like a very light-weight virtual machine. It acts like its own operating system and code running in a container doesn’t even know it’s being run inside one while not being able to see anything outside the container. If one of your PHP scripts is insecure and gives someone effective shell access, they can only access files on the server that you’ve given the container access to.
Unlike a virtual machine, if two different websites are in completely different containers but use the same NGINX or PHP versions, disk space and RAM is shared between the two containers.
Because each image is self-contained, moving the website to a different server is easy. The application doesn’t rely on the server’s installed PHP version and it doesn’t care what packages are installed on the server. If you want to move a Dockerized application to a different server, it’s as simple as copying all the website files and launching the application.
You can run as many Docker images on the server as you like, each with their own PHP version, web server software, database and associated files.
Setting Things Up
That’s the theory out of the way. Now let’s jump in and create a server using Docker.
Before we start, you’ll need to download and install Docker. Head over to the Docker website, then download and install it for your operating system.
If you’re on Linux, you should install the
docker-compose packages through your distribution’s package manager. Depending on your distribution, you may need to:
Add your user to the
dockergroup as outlined in the Docker manual here.
systemctl start docker.serviceand enable it with
systemctl enable docker.
If you’re on Windows or macOS, the installer will do this for you.
Secondly, because we’re going to be running a web server inside Docker and forwarding some ports if you already have a web server (Apache, NGINX, XAMPP, IIS, etc.) or MySQL running on your machine, stop them before continuing.
A web server usually consists of multiple different programs — such as NGINX, PHP and MySQL. In Docker’s terminology, each program you wish to install is a service.
There are several ways of creating these services in Docker. I’ll cover the most user friendly. Docker supports creating a configuration file using YAML (Yet Another Markup Language).
Although you can type in all the options on the command line, I recommend using the YAML configuration file for several reasons:
It’s a lot easier to read/understand.
You don’t have to re-type several long commands every time you want to run the server.
You can track changes to the file with Git.
docker-compose.yml for NGINX
Docker provides a tool called
docker-compose that takes a configuration file called
docker-compose.yml and launches the services listed inside it. Let’s start by adding a web server, NGINX.
Firstly, create a folder somewhere on your computer that will store your website. You’ll need to go back to this folder regularly so remember where it is. Create
docker-compose.yml with the following contents:
version: '3' services: web: image: nginx:latest ports: - "80:80"
Let’s take a look through the configuration one line at a time:
docker-compose which version of the YAML specification to use.
3 is the latest, and different versions have a slightly different specification, keywords and structure.
The next line,
services:, will be followed by a list of all the services you want to run.
In our example so far, there’s just one service called
web (you can call this anything you like) using the official NGINX image
nginx:latest. Note that the indentation using spaces (not tabs!) matters. YAML relies on the nesting level to determine the structure of the file.
If you wanted to specify a different NGINX version, you could specify that here like so:
version: '3' services: web: image: nginx:1.18.0 ports: - "80:80"
I recommend using
latest unless you have a good reason to use an earlier version.
ports block sets up port forwarding. It forwards
80 on the local machine to
80 on the image. Any request on the host machine to
http://127.0.0.1 will be forwarded to the NGINX server running in the container.
Setting Up a Modern PHP Development Environment with Docker