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.
Learn to code with our beginner friendly tutorials
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.
Pre-Requisites: Docker & Docker-compose installed.
First step: Get a simple PHP script to run with Dockerfile.
If you take a look at the PHP Docker docs 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 Dockerfile
with this code:
FROM php:7.4-cli
COPY . /usr/src/myapp
WORKDIR /usr/src/myapp
CMD [ "php", "./index.php" ]
This code is the setup to create the image we'll use. This setup pulls in PHP 7.4 with some command line stuff.
The COPY . /usr/src/myapp
copies the contents of the current directory into /usr/src/myapp inside the Docker container. The next line: WORKDIR /usr/src/myapp
sets the /usr/src/myapp as the "working directory", kind of like you would cd /to/your/project.
Then from inside the /usr/src/myapp folder we run the command $ php ./index.php
where index.php is our script. So that means we need a PHP script. Let's create our index.php
file inside the project directory like this:
<?php
echo "Hello from the docker container";
Now we can continue with their instructions, which is to build and run the docker image:
$ docker build -t my-php-app .
$ docker run -it --rm --name my-running-app my-php-app
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:
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. To run the Apache/php image WITHOUT a dockerfile, run this command: (Note the first line is for Linux/Mac users and the 2nd line is for the lonely Windows users
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
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 php:7.2-apache
Note that Windows doesn't have a $PWD command, so I had to manually put my path for this to work.
Okay, so we setup to have our script run on port 80, so go ahead and visit http://localhost:80 and you should see your script running in the browser.
PHP and docker-compose
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 docker ps
to get the container id and then run $ docker stop container_id
but I'm just going to stop all containers like this:
docker stop $(docker ps -a -q)
Now create a docker-compose.yml
file in the root of your project and put the following inside:
version: '3.1'
services:
php:
image: php:7.4-apache
ports:
- 80:80
volumes:
- ./src:/var/www/html/
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)
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 /var/www/html
folder. Now let's move our script into the ./src folder on our local machine. My project structure now looks like this:
Now run docker-compose up -d
from inside the project root and visit localhost:80 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.
Now let's shut down our container with docker-compose down
.
The next step is to setup MySQL and Adminer (a DB access tool) 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 docker-compose.yml
file should look like now:
# 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
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)
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.
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.
Let's also setup Mysqli to work inside our PHP container. Here's my Dockerfile code:
FROM php:7.4-apache
RUN docker-php-ext-install mysqli
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 docker-compose up -d
again.
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.
Select "Create Database"
Name it and hit save. I named mine "company1"
Then select "new item" to add a few items to your table.
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.
<?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[] = $data;
}
}
foreach ($users as $user) {
echo "<br>";
echo $user->name . " " . $user->fav_color;
echo "<br>";
}
Now here is the result of our code when we run it in the browser. :
Persistent Mysql using Volumes:
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. Without a volume your data will be erased every time you stop the containers. 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"
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:
Note the indentation. volumes will be at the same indentation level as "services".
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.
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.
Top comments (8)
I was really stuck trying to make a docker-compose with httpd and php separated, but you helped me a lot
Also, in case anyone have an "internal server error", add
RUN a2enmod rewrite
to your Dockerfile
For macOS users that cannot connect to the localhost:
The docker containers do not run on the host machine itself but in a VM.
I used the following workaround:
1.) Find the IP with the command 'docker-machine ip default' in the terminal.
2.) The command returns something like '192.168.99.100'.
3.) Use this IP instead of 'localhost'. Access the port 80 by typing '192.168.99.100:80' into the browser. Or '192.168.99.100:8080' for the port 8080.
This is the best tutorial for PHP and mysql combination. I tried many but none worked and after this I was able to run, and could understand all details from video. Many thanks for posting this tutorial.
Good tutorial!
If you want a PWD-like in Windows you can use %cd% system variable ;)
%cd%
works forcmd
, but if you're in PowerShell you can use$PWD
:)Thank you for the tutorial.
I was getting "Could not open input file: ./index.php" when I add Mysql part to docker-compose.yml. I added "working_dir: /var/www/html/" to my Dockerfile. Now, it is not finding mysqli class.
Solved:
Do not forget to rebuild the container after you add mysqli
docker-compose up --build
Brilliant this tutorial. To whom is starting with Docker this is a
outstanding reference.
Thank you so much!
I am back to PHP because a very old project and this article really helped me.
I adition: I really like Adminer.