My favorite Laravel development environment, with Docker, Nginx, PHP-FPM Xdebug in VSCode
Fabio
Posted on July 8, 2022
In my last post I showed how you can set up XDebug for a simple dev environment, which used php artisan serve
. However, what I am showing today is actually more perfomant. Once you understand the set up, it will probably soon be your favorite Laravel dev environment as well.
The setup can be found in this repository.
Table Of Content
Why should I use the set up
The set up is great, because you will be able to:
- Use Nginx and php-fpm to process requests much faster
- Use the debugger with Nginx calls
- Use the debugger for the queue
- Have a redis cache set up
- Have a database set up
- Have a
php artisan serve
server as fallback
Nginx with php-fpm enables parallel processing of requests, while with just the php artisan serve
you will only be able to process requests in sequence.
Requirements
The only requirement is that you have a newer version of docker installed. (In older docker versions you might have to manually install docker-compose as well).
Set up Guide
Break Down
- Create the Dockerfiles - I am gonna use an image that I have created for this guide (GitHub or from hub.docker)
- Create the XDebug configs
- Set the .env vars
- Create the docker-compose
- Create the Nginx config
- Set the correct permissions for
storage
andbootstrap
- (optional) Add a library for Redis
Dockerfiles and Xdebug Configs
We are going to need 3 files, 2 Dockerfiles
and one docker-compose.yml
file.
First the main Dockerfile for the actual app container:
FROM snakepy/laravel-dev-image:php7.4-a696761a6b37e2480ba83edc4edee9a7632f3332
WORKDIR /app
COPY .docker/xdebug.ini /etc/php/7.4/cli/conf.d/99-xdebug.ini
RUN npm install
You can see we are pulling the image which comes with all the plugins you will need. (I plan to release a PHP 8 image as well, let me know if you guys also are interested in a production image βοΈ). Next we copy configs for debugging on the php artisan serve
container. They can be found at (.docker/xdebug.ini):
zend_extension = xdebug
xdebug.remote_enable=on
xdebug.remote_autostart = 1
xdebug.mode=develop,gcstats,coverage,profile,debug
xdebug.idekey=docker
xdebug.start_with_request=yes
;xdebug.log=/tmp/xdebug.log
xdebug.client_port=9003
xdebug.client_host='host.docker.internal'
xdebug.discover_client_host=0
Now we need to define a second docker file for serving the app with PHP-FPM. We need to do that because Nginx does not come with a PHP plugin. If we'd use Apache we could directly serve from Apache.
FROM php:7.4-fpm
RUN pecl install xdebug-2.9.2 \
&& docker-php-ext-enable xdebug
RUN docker-php-ext-install mysqli pdo pdo_mysql
COPY .docker/xdebug-nginx.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
Note here we are not pulling the image I have prepared, it would be overkill, hence we are going with the default image from PHP. The Xdebug configs are being copied as well, but they are slightly different. See at (.docker/xdebug-nginx.ini):
zend_extension = xdebug
xdebug.remote_enable=on
xdebug.remote_autostart = 1
xdebug.default_enable=1
xdebug.mode=develop,gcstats,coverage,profile,debug
xdebug.idekey=docker
xdebug.start_with_request=yes
;xdebug.log=/tmp/xdebug.log
;xdebug.remote_log=/tmp/xdebug.log
xdebug.client_host='host.docker.internal'
xdebug.discover_client_host=0
xdebug.remote_handler=dbgp
ENV Vars and docker-compose
This is by far the hardest part to get right. The docker-compose file I am going to present requires some .env vars, hence I am going to show these first.
Please set in .env following vars:
# Docker Ports
DOCKER_EXPOSED_DB_PORT=3307
DOCKER_EXPOSED_NGINX_PORT=10000
DOCKER_EXPOSED_ARTISAN_SERVE_PORT=8000
## can be retrieved with hostename -I
DOCKER_NGINX_LOCAL_IP=
## DO NOT CHANGE!If you change this, you also need to change DB_HOST and REDIS_HOST
DOCKER_APP_CONTAINER_NAME="laravel-dev-to"
DB_CONNECTION=mysql
DB_HOST=laravel-dev-to-db
DB_PORT=3306
DB_DATABASE=test
DB_USERNAME=test
DB_PASSWORD=Password123!
REDIS_HOST=laravel-dev-to-cache
REDIS_PASSWORD=REDIS_PASSWORD
REDIS_PORT=6379
Note that DOCKER_APP_CONTAINER_NAME
is used as a prefix in the docker-compose hence the naming convention to DB_HOST
and REDIS_HOST
must be maintained.
Let's look at the docker-compose.yml:
version: '3.8'
services:
app:
container_name: ${DOCKER_APP_CONTAINER_NAME}
extra_hosts:
- "host.docker.internal:host-gateway"
build:
context: .
dockerfile: Dockerfile
command: php artisan serve --host=0.0.0.0
volumes:
- .:/app
ports:
- ${DOCKER_EXPOSED_ARTISAN_SERVE_PORT}:8000
depends_on:
- db
- composer
networks:
- proxynet
env_file:
- .env
queue:
container_name: ${DOCKER_APP_CONTAINER_NAME}-queue
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- .:/app
build:
context: .
dockerfile: Dockerfile
command: 'php artisan queue:work'
environment:
- REDIS_HOST=${REDIS_HOST}
- REDIS_PASSWORD=${REDIS_PASSWORD}
- REDIS_PORT=${REDIS_PORT}
depends_on:
- app
- cache
- db
- composer
networks:
- proxynet
db:
container_name: ${DOCKER_APP_CONTAINER_NAME}-db
platform: linux/x86_64
image: mysql:8.0
restart: "no"
environment:
MYSQL_ROOT: root
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
ports:
- ${DOCKER_EXPOSED_DB_PORT}:3306
volumes:
- db_data:/var/lib/mysql
networks:
- proxynet
env_file:
- .env
cache:
container_name: ${DOCKER_APP_CONTAINER_NAME}-cache
command: redis-server --requirepass ${REDIS_PASSWORD}
image: redis:5.0
ports:
- :6379
volumes:
- cache_data:/data
networks:
- proxynet
nginx:
image: nginx:stable-alpine
container_name: ${DOCKER_APP_CONTAINER_NAME}-nginx
ports:
- ${DOCKER_EXPOSED_NGINX_PORT}:80
volumes:
- ./:/var/www/html
- ./.docker/nginx/default.conf:/etc/nginx/conf.d/default.temp
depends_on:
- app
- composer
- db
- cache
command: /bin/sh -c "envsubst '$$DOCKER_NGINX_LOCAL_IP $$DOCKER_APP_CONTAINER_NAME' < /etc/nginx/conf.d/default.temp > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"
env_file:
- .env
networks:
- proxynet
php-fpm:
extra_hosts:
- "host.docker.internal:host-gateway"
build:
dockerfile: Dockerfile.PHP-FPM
container_name: ${DOCKER_APP_CONTAINER_NAME}-php-fpm
volumes:
- ./.docker/xdebug-nginx.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
- ./:/var/www/html
ports:
- ":9000"
depends_on:
- app
- composer
- db
- cache
networks:
- proxynet
composer:
build:
context: .
dockerfile: Dockerfile
container_name: ${DOCKER_APP_CONTAINER_NAME}-composer
volumes:
- .:/app
command: composer install
networks:
- proxynet
env_file:
- .env
volumes:
db_data:
driver: "local"
cache_data:
driver: "local"
networks:
proxynet:
name: portal
Okay I am going to break down the docker-compose file so actually understand what going on there. It will build the following containers:
- app the basic Laravel dev app
- db a MySQL database
- cache Redis cache
- queue basic Laravel dev queue worker
- Nginx the web server for serving to php-fpm and serving static content
- php-fpm server for executing PHP code for Nginx
- composer runs an install on container boot and will the exit
All these containers are being wrapped into a proxynet in case you want to use the same network in another compose file like a Vue frontend.
The variables from the .env file are being used to determine which ports are going to be exposed. In my example we are exposing php artisan serve
on port 8000 and nginx
on port 10000. The mysql db
will be exposed on port 3307.
The last thing I want to point out about the compose file is the use of:
extra_hosts:
- "host.docker.internal:host-gateway"
Which basically tells docker to resolve docker-container-names to their network IP address.
In the next section we are going to take a closer look at the Nginx compose section and configs.
NGINX Configs
Let us talk about the Nginx volumes. Nginx mounts two volumes. ./:/var/www/html
will be used for static file serving, like images. ./.docker/nginx/default.conf:/etc/nginx/conf.d/default.temp
this volume are the actual nginx configs. If you know Nginx then you will notice that we actually put a "wrong" file name into the image default.temp
. We do not mount the Nginx to the end/correct location because we require envsubst
to run over it and replace two variables in the nginx.conf file.Γ¨nvsubst
has the ability to run over a config file and replace only the variables with the names which were provided to it. I think this answer describes the issue very well. In the command envsubst
we define the correct output path for nginx and run the Nginx deamon.
These Varibles need to be replaced and the other should not be touched:
- $DOCKER_APP_CONTAINER_NAME needs to replaced so nginx can proxy all php requests to php-fpm
- $DOCKER_NGINX_LOCAL_IP is required for XDebug
β I had a lot of difficulty getting it right, but note that you need to escape the variables for docker with two dollar signs $$
. β
Example:
envsubst '$$DOCKER_NGINX_LOCAL_IP $$DOCKER_APP_CONTAINER_NAME'
Here is the Nginx config template you need to copy into your project, I have them at (.docker/nginx/default.conf):
server {
listen 80;
# this path MUST be exactly as docker-compose.fpm.volumes,
# even if it doesn't exist in this dock.
root /var/www/html/public;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/.+\.php(/|$) {
fastcgi_pass $DOCKER_APP_CONTAINER_NAME-php-fpm:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PHP_VALUE "xdebug.remote_autostart=1
xdebug.remote_enable=1
xdebug.remote_host=$DOCKER_NGINX_LOCAL_IP";
}
}
Set Permissions
Simply give the ./storage and ./bootstrap folder these permissions:
chmod -R 777 ./storage
chmod -R 777 ./bootstrap
Redis Lib (optional)
I am going to connect to redis using the libary predis. Simply add the following to your compose.json
"require": {
...
"predis/predis": "^1.1",
...
}
and inside config/databse.php:122 I switched to use predis.
First Run π
If you have followed the previous steps now is the moment of truth: π¦ π¦
docker compose run
If this is the first boot and you had no vendor folder prior to this - then wait till the compose container exits and shut the container down and rerun docker compose up
.
now you should be able to hit πππ:
http://localhost:8000
and
http://localhost:10000
If everything is opening up as expected then you can continue with the IDE set up for XDebug. If not then have a look a the github repo or leave me a comment and I can try to assist you. Also have a look the troubleshooting section.
Set Up VS Code
Now the set up for VS Code to actually use the debugger you need first to install the following extension.
Then copy over the launch.json file I have prepared. You will find multiple configurations in there. I will go over one so you understand whats going on.
{
"name": "Listen for Xdebug inside docker",
"type": "php",
"request": "launch",
"hostname": "0.0.0.0",
"pathMappings": { "/app": "${workspaceRoot}/" },
"port": 9003,
"log": true
},
-
name
shows up in the debugger drop down -
hostname
needs to be set to 0.0.0.0 so the server can find the XDebug report from localhost -
pathMappings
is basically a map where the folders are placed on the server =>{"path_to_app_inside_docker": "${workspaceRoot}/"}
theworkspaceRoot
is automatically set by VS Code -
port
the port on which XDebug will answer
If you want to debug now, you will need to run docker compose up
once the containers are running you can choose from the drop down, on which requests you want to listen to. I usually just launch two debugger instances:
- "Listen for Xdebug inside docker" and
- "Listen for Xdebug inside docker nginx"
After the debugger is launched you should be able to hit break points, if requests are going to http://loclahost:8000 or http://localhost:10000
To test this insert this into api.php
:
Route::get('/test', function () {
return response()->json(['message' => 'Visit my portfolio site at snake-py.com'], 200);
});
The set a a break point on the return statement line and try to hit the route http://localhost:8000/api/test or http://localhost:10000/api/test
Troubleshooting
There is actually a lot of stuff which might go wrong during your first set up. I just want to mention here a few ways which helped me debug. (I also try to keep this a little updated). Feel free to open a GitHub issue or comment here for support.
Debugger does not connect?
Enable the following config, which will output a log file wiht an error you can google.
xdebug.log=/tmp/xdebug.log
xdebug.remote_log=/tmp/xdebug.log
The log file will be in the app container or the php-fpm container depending on which port your hitting. To get into the php-fpm container is no bash installed yo you need execute:
docker exec -it <php-fpm-container-name> bin/sh
Database or cache does not connect properly?
If you are sure that the containers can reach each other and that the names you have provided in the .env file are correct you can go inside the app container and run a config clear.
docker ps # to get the name of the app container
docker exec -it <container_name> bash
php artisan config:clear
php artisan cache:clear
php artisan route:clear
php artisan migrate
If one of these commands fails then you usually did not provide the correct name or port in the .env file.
Nginx is failing on boot?
Did you set the correct local ip address? Is the php-fpm container name correctly prefixed? Check with docker ps
I hope I could help you with this set up let me know what you guys might think! π
Posted on July 8, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.