43 0 2MB
osteel's blog
Web development resources
Docker for local web development, part 1: a basic LEMP stack Last updated: 2022-02-19 :: Published: 2020-03-04 :: [ history ]
Been here before? Get notified of future posts by email:
Part 1: a basic LEMP stack
you are here
Part 2: put your images on a diet Part 3: a three-tier architecture with frameworks Part 4: smoothing things out with Bash Part 5: HTTPS all the things Part 6: expose a local container to the Internet Part 7: using a multi-stage build to introduce a worker Part 8: scheduled tasks Conclusion: where to go from here Subscribe to email alerts at the end of this article or follow me on Twitter to be informed of new publications.
In this post In this series In this post The first steps Identifying the necessary containers Docker Compose Nginx PHP MySQL phpMyAdmin Domain name Environment variables Commands summary and cleaning up your environment Conclusion
The first steps I trust you've already read the introduction to this series and are now ready for some action.
The first thing to do is to head over to the Docker website and download and install Docker Desktop for Mac or PC, or head over here for installation instructions on various Linux distributions. If you're on Windows, make sure to install Windows Subsystem for Linux (WSL 2), and to configure Docker Desktop to use it. The second thing you will need is a terminal. Once both requirements are covered, you can either get the final result from the repository and follow this tutorial, or start from scratch and compare your code to the repository's whenever you get stuck. The latter is my recommended approach for Docker beginners, as the various concepts are more likely to stick if you write the code yourself. Note that this post is quite dense because of the large number of notions being introduced. I assume no prior knowledge of Docker and I try not to leave any detail unexplained. If you are a complete beginner, make sure you have some time ahead of you and grab yourself a hot drink: we're taking the scenic route.
Identifying the necessary containers Docker recommends running only one process per container, which roughly means that each container should be running a single piece of software. Let's remind ourselves what the programs underlying the LEMP stack are: L is for Linux; E is for Nginx; M is for MySQL; P is for PHP.
Linux is the operating system Docker runs on, so that leaves us with Nginx, MySQL and PHP. For convenience, we will also add phpMyAdmin into the mix. As a result, we now need the following containers: one container for Nginx; one container for PHP (PHP-FPM); one container for MySQL; one container for phpMyAdmin. This is fairly straightforward, but how do we get from here to setting up these containers, and how will they interact with each other?
Docker Compose Docker Desktop comes with a tool called Docker Compose that allows you to define and run multi-container Docker applications (if your system runs on Linux, you will need to install it separately). Docker Compose isn't absolutely necessary to manage multiple containers, as doing so can be achieved with Docker alone, but in practice it is very inconvenient to do so (it would be similar to doing long division while there is a calculator on the desk: while it is certainly not a bad skill to have, it is also a tremendous waste of time). The containers are described in a YAML configuration file and Docker Compose will take care of building the images and starting the containers, as well as some other useful things like automatically connecting the containers to an internal network. Don't worry if you feel a little confused; by the end of this post it will all make sense.
Nginx The YAML configuration file will actually be our starting point: open your favourite text editor and add a new docker-compose.yml file to a directory of your choice on your local machine (your computer), with the following content: 1 2 3 4 5 6 7 8 9 10
version: '3.8' # Services services: # Nginx Service nginx: image: nginx:1.21 ports: - 80:80
The version key at the top of the file indicates the version of Docker Compose we intend to use (3.8 is the latest version at the time of writing). It is followed by the services key, which is a list of the application's components. For the moment we only have the nginx service, with a couple of keys: image and ports . The
former indicates which image to use to build our service's container; in our case, version 1.21 of the Nginx image. Open the link in a new tab: it will take you to Docker Hub, which is the largest registry for container images (think of it as the Packagist or PyPI of Docker).
Why not use the latest tag? You will probably notice that all images have a latest tag corresponding to the most up-to-date version of the image. While it might be tempting to use it, you don't know how the image will evolve in the future – it is very likely that breaking changes will be introduced sooner or later. The same way you do a version freeze for an application's dependencies (via
composer.lock for PHP or requirements.txt in Python, for
example), using a specific version tag ensures your Docker setup won't break due to unforeseen changes.
Much like a Github repository, image descriptions on Docker Hub usually do a good job at explaining how to use it and what the available versions are. Here, we are looking at Nginx's official image: Docker keeps a curated list of "official" images (sometimes maintained by upstream developers, but not always), which I always use whenever possible. They are easily recognisable: their page mentions Docker Official Images at the top, and Docker Hub separates them clearly from the community images when doing a search:
Note the "Verified Content" at the top
Back to docker-compose.yml : under ports , 80:80 indicates that we want to map our local machine's port 80 (used by HTTP) to the container's. In other words, when we will access port 80 on our local machine (i.e. your computer), we will be forwarded to the port
80 of the Nginx container. Let's test this out. Save the docker-compose.yml file, open a terminal and change the current directory to your project's before running the following command: $ docker compose up -d
It might take a little while as the Nginx image will first be downloaded from Docker Hub. When it is done, open localhost in your browser, which should display Nginx's welcome page:
Congratulations! You have just created your first Docker container.
Let's break down that command: by running docker compose up -d , we essentially asked Docker Compose to build and start the
containers described in docker-compose.yml ; the -d option indicates that we want to run the containers in the background and get our terminal back. You can see which containers are currently running by executing the following command: $ docker compose ps
Which should display something similar to this:
To stop the containers, simply run: $ docker compose stop
At this point, you might be wondering what the difference is between a service, an image and a container. A service is just one of your application's components, as listed in dockercompose.yml . Each service refers to an image, which is used to
start and stop containers based on this image. To help you grasp the nuance, think of an image as a class, and of a container as an instance of that class. Speaking of OOP, how about we set up PHP?
PHP By the end of this section, we will have Nginx serving a simple index.php file via PHP-FPM, which is the most widely used
process manager for PHP.
Not a PHP fan? As mentioned in the introduction, while PHP is used on the server side throughout this series, swapping it for another language should be fairly straightforward.
Replace the content of docker-compose.yml with this one: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
version: '3.8' # Services services: # Nginx Service nginx: image: nginx:1.21 ports: - 80:80 volumes: - ./src:/var/www/php - ./.docker/nginx/conf.d:/etc/nginx/conf.d depends_on: - php # PHP Service php: image: php:8.1-fpm working_dir: /var/www/php volumes: - ./src:/var/www/php
A few things going on here: let's forget about the Nginx service for a moment, and focus on the new PHP service instead. We start from the php:8.1-fpm image, corresponding to the tag 8.1-fpm of PHP's official image, featuring version 8.1 and PHP-FPM. Let's skip working_dir for now, and have a look at volumes . This section
allows us to define volumes (basically, directories or single files) that we want to mount onto the container. This essentially means we can map local directories and files to directories and files on the
container; in our case, we want Docker Compose to mount the src folder as the container's /var/www/php folder.
What's in the src/ folder? Nothing yet, but that's where we are going to place our application code. Once it is mounted onto the container, any change we make to our code will be immediately available, without the need to restart the container. Create the src directory (at the same level as dockercompose.yml ) and add the following index.php file to it: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Hello there