Unlike other ecosystems like .NET or Java in which containerizing an application for local development might seem more like a burden than a feature when it comes to PHP this is a necessity as well as a welcome bonus of mirroring the production environment very closely.
Some applications might require a PHP extension that is not present on the local system or there might be some specific php.ini configuration that is set on the production server, which is considered as part of the infrastructure rather than the application itself but can cause problems because of the inconsistency (I had this happen to me several years ago). Using Docker can solve these types of problems and has also the added benefit of being a really good way to develop applications, an argument for this being the good support for remote debugging in IDEs.
Overview
I will go through setting up the entire stack, that is PHP 8.1 with fpm, nginx and, mysql as the database and at the end, we will do basic Symfony installation to test everything together.
If you don't just want to check out the repo with the entire setup you can find it on Github
Setting up docker-compose configuration
There are two files that tell docker-compose what to spin up. The first is docker-compose.yml and the other is docker-compose.override.yml. The override file is responsible for exposing the host using the hostname dockerhost
. This is useful for both debugging and for referencing other containers more easily.
version: '3.5'
services:
devbox:
container_name: devbox-nginx
build:
context: ./docker/nginx
dockerfile: Dockerfile
ports:
- "9001:80"
volumes:
- .:/app:cached
restart: unless-stopped
depends_on:
- devbox-service
devbox-service:
container_name: devbox-service
build:
context: .
volumes:
- .:/app:cached
- ./docker/service/php.ini:/usr/local/etc/php/conf.d/99-app.ini
- ./docker/service/www.conf:/usr/local/etc/php-fpm.d/www.conf
restart: unless-stopped
environment:
XDEBUG_CONFIG: ${XDEBUG_CONFIG}
APP_ENV: ${APP_ENV}
APP_DEBUG: ${APP_DEBUG}
APP_SECRET: ${APP_SECRET}
env_file:
- .env
- .env.local
depends_on:
- mysql
mysql:
image: mysql:8.0
container_name: devbox-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: database
ports:
- "3308:3306"
volumes:
- database-volume:/var/lib/mysql
volumes:
database-volume:
driver: "local"
Adding the Dockerfiles
There are two Dockerfiles used. One for php-fpm and the other for nginx. The one for the app itself is the php-fpm one and it's found in the root of the project and the other one is found in docker/nginx/Dockerfile. The nginx Dockerfile just copies the nginx configuration defined in default.conf into the container. The nginx configuration is quite apart from the fact that the name of the service defined in docker-compose was specified
Dockerfile for nginx
FROM nginx:stable
COPY default.conf /etc/nginx/conf.d/default.conf
Dockefile for php-fpm
FROM php:8.1-fpm-alpine
LABEL maintainer="alexandrunastase@github"
LABEL description="Devbox Docker image"
# User build args
ARG APP_ENV="prod"
ARG APP_DEBUG="0"
ARG APP_LOG="php://stdout"
# Environment variables
ENV APP_ENV=${APP_ENV}
ENV APP_DEBUG=${APP_DEBUG}
ENV APP_LOG=${APP_LOG}
ENV XDEBUG_CONFIG=""
ENV COMPOSER_NO_INTERACTION=1
# Add PHP user
ARG PHP_USER_ID=1000
ARG PHP_GROUP_ID=1000
RUN set -x \
&& addgroup -g $PHP_GROUP_ID -S php \
&& adduser -u $PHP_USER_ID -D -S -G php php
# Install dependencies
RUN set -ex \
&& docker-php-source extract \
&& apk add --update --no-cache \
${PHPIZE_DEPS} \
curl \
# Runtime deps
icu-dev icu-libs \
libzip-dev zlib-dev \
libxml2-dev \
oniguruma-dev \
&& pecl install xdebug \
&& docker-php-ext-install intl opcache pdo_mysql zip bcmath mbstring sockets pcntl soap sockets ctype > /dev/null \
&& docker-php-ext-enable intl opcache pdo_mysql zip bcmath mbstring sockets pcntl soap sockets ctype \
&& apk del ${PHPIZE_DEPS} \
&& docker-php-source delete
# Copy configuration files
COPY ./docker/service/www.conf /usr/local/etc/php-fpm.d/www.conf
COPY ./docker/service/php.ini $PHP_INI_DIR/conf.d/99-app.ini
COPY ./docker/service/xdebug.ini $PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini
# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY --chown=php . /app
WORKDIR /app
USER php
Creating the Makefile
I find a Makefile a useful addition to any docker-compose setup, as it makes it much easier to access the common commands without needing to remember how a container was named or needed to search to the shell history.
Note: When editing Makefiles make sure to always use tabs instead of spaces especially when indenting commands
.PHONY: run
run:
@if [ ! -e ".env.local" ]; then\
cp .env .env.local; \
fi
@docker-compose up -d
@echo "Service is running on http://localhost:9001"
.PHONY: install
install:
@docker-compose exec --user="php" -T devbox-service composer install
.PHONY: stop
stop:
@docker-compose stop
.PHONY: enter
enter:
@docker-compose exec --user="php" devbox-service /bin/sh
.PHONY: enter-as-root
enter-as-root:
@docker-compose exec --user="root" devbox-service /bin/sh
.PHONY: test
test:
@docker-compose exec --user="php" -T devbox-service /bin/sh -c 'APP_ENV="test" ./bin/phpunit --testdox'
.PHONY: destroy
destroy:
@docker-compose down --rmi local
Adding Symfony and testing everything
To test the entire setup we can setup a Symfony application. You can find instructions to do so here: https://symfony.com/doc/current/setup.html . I went with the LTS version which is 5.4 at the time of the writing.
Note: I also updated the composer file to make sure the database is created. You can skip this if you have another way to make that happen.
Setting up xDebug
Debugging can be enabled by uncommenting the contents of the file ./docker/service/xdebug.ini
These are the steps to configure xDebug on PHPStorm:
- Choose
PHP Remote Debugging
as CLI interpreter. Make sure local interpreter is removed - Choose
Docker Compose
as the configuration type anddevbox-service
as the service - Lifecycle should be
Connect to existing container
Working demo
In the docker-compose the port 9001 is mapped for the localhost so you can check everything is working after running:
make run
to se tup the containers
make install
to install all the composer packages.
There is one endpoint defined called http://localhost:9001/healthz
which should return a 200 status code
In order to run the tests, you can use
make test
and for running other ad-hoc commands like requiring another composer package you can do
make enter
Tested using
- Ubuntu 21.10
- docker version : 20.10.14
- docker-compose version : 1.29.1
cross-posted from my blog: Building a simple php dockerized environment
Top comments (2)
Docker is there with vast application but still some people prefer "easier" method to setup php environment like using AMP packages, especially beginners. What do you think about this?
I mean it depends on people's needs also. If you want to do a simple hello world to try things out, the classic setup can be enough. But in my experience, if you want to have a common setup between multiple people, eventually have a CI/CD pipeline, a production environment, and debugging working out of the box, a Docker setup might be a better choice. Plus, if you're on a Linux/Mac environment, the only prerequisites one would need would be to install docker, docker-compose, and make. That's it. You do
make run && make install
, and everything is set up. Seems quite simple to me and the advantage of having a reproducible environment is quite big I would say, especially when you are a beginner and you might need some assistance here and there.