Using Traefik to develop locally with Docker (Part 3/x)
This is a continuation of the series explaining how to leverage Traefik to have a flexible setup for local development of your projects using docker. If you read and followed the previous parts, you should have now a valid setup to spin up any new docker project that listen on both HTTP (80) and HTTPS (443). Now let’s put everything together and show how would a new project would look like within this setup. I’m going to do it using standard images to explain all the concepts, on the following post I’ll do the same thing but with a bunch of “tricks” to make all the cumbersome stuff from docker way easier, so don’t fret at the strange commands or processes, I’m not doing it like this on my day to day.
The Project#
In order to demonstrate this, let’s go with a new Laravel app created from scratch. Let’s assume we don’t have
(sidenote:
No composer, no PHP, no database. Docker and the docker-compose plugin are the only things needed and I'm assumming you have those already if you completed the previous parts of the series.
)
. Let’s start by creating a new folder for our project, as $DEITY
intended for any software project, initializing a git repo. Then, let’s create the docker-compose.yml
, that’ll be our starting point.
$ mkdir my-new-project
$ git init
$ touch docker-compose.yml
Install with composer#
Let’s start from the beginning. We need to create a new Laravel project and, as mention earlier, we are assuming we don’t have anything installed on the computer except Docker, so let’s start our docker-compose.yml
by adding the composer image:
# docker-compose.yml
services:
# Let's include a composer image on its own profile so we can install stuff
composer_app:
image: composer:latest
volumes:
- ./:/app
# - ~/.config/composer:/tmp
Here, we just add the latest available composer image from Docker Hub and mount the current folder (./
) into the /app
folder from the container. I’ve left the second volume commented out because, in theory, we don’t have a local composer install,
(sidenote:
Even if you don't have composer
installed locally and never plan to, it may make sense to create a folder on your local filesystem to share all the composer config and cache with all the containers so you don't need to download the packages every time.
)
, we could mount its config folder to the /tmp
folder in the container (which is the value of $COMPOSER_HOME
according to the image docs) and share th config and the cache with the container. Anyway, with our docker-compose.yml
ready we can now run the following:
docker compose run --user $(id -u):$(id -g) --rm composer_app \
composer create-project laravel/laravel app
The command above will create a new Laravel app on the app
folder, we have two options now, either move the docker-compose.yml
inside it or move all the newly created files one level up and delete the app
folder. In order to do the second you can do:
mv app/{.,}* . && rm -r app
Now that we have a new Laravel app, let’s see what we need to do in order to get access to it.
Web access#
Ok, if you’ve ever worked with PHP, you’ll know there are different options to run a web PHP app,
(sidenote:
On modern Apache versions, it's recommended to use PHP-FPM with proxy_fcgi
but whatever.
)
, any webserver and using PHP-FPM as a FastCGI, or even using other modern options like FrankenPHP. Any option should be valid for this setup. Just to pick one, let’s start with Nginx + PHP-FPM.
# The webserver, good ol' nginx
web_app:
image: nginx:stable-alpine
working_dir: /var/www/app
volumes:
- ./:/var/www/app
- ./resources/docker/vhost.conf:/etc/nginx/conf.d/default.conf:ro
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.redirect-2-https.redirectscheme.scheme=https"
- "traefik.http.routers.insecure-web.entrypoints=web"
- "traefik.http.routers.insecure-web.rule=Host(`laravel-app.local.example.com`)"
- "traefik.http.routers.insecure-web.middlewares=redirect-2-https@docker"
- "traefik.http.routers.web.entrypoints=web-secure"
- "traefik.http.routers.web.rule=Host(`laravel-app.local.example.com`)"
- "traefik.http.routers.web.tls=true"
profiles: [ "web" ]
# PHP-FPM to run our PHP code
fpm_app:
image: php:8.3-fpm-alpine
working_dir: /var/www/app
volumes:
- ./:/var/www/app
# - ./resources/docker/php.ini:/usr/local/etc/php/conf.d/zz-conf.ini:ro
# - ./resources/docker/xdebug.ini:/usr/local/etc/php/conf.d/20-xdebug.ini:ro
profiles: [ "web" ]
# Composer from before
composer_app:
image: composer:latest
volumes:
- ./:/app
profiles: [ "composer" ]
networks:
default:
external: true
name: gateway
There are some things from the file I’d like to comment on. First, I’m using profiles because they are super handy when you don’t want all your images to run at the same time. Then, when possible, I’m using the alpine version of the images, just because they are smaller and I like musl
. This is not necessary and you can run the debian versions or whatever others you prefer. Another thing to notice, I’ve used laravel-app.local.example.com
which, obviously is not a valid domain, you should replace it with one you own and for which you’ve generated TLS certs, or use one from backloop.dev. The other new thing from the previous post, is that I’ve included two new labels:
traefik.http.middlewares.redirect-2-https.redirectscheme.scheme=https
traefik.http.routers.insecure-web.middlewares=redirect-2-https@docker
RedirectScheme is a Traefik middleware that allows you to, well, redirect the request if the request scheme is different from the configured scheme. In this case, requests coming into port 80 (insecure-web
router) not using https
will get redirected to https
.
With that out of the way, let’s go through the file:
- We have an
nginx
image with all the labels as explained on the previous post to let traefik know that we want HTTP traffic pointing to the exposed port of the container (80 in this case). We are mounting the current folder (./
) into the/var/www/app
folder on the container. Then, there’s the./resources/docker/vhost.conf
which we still need to create, (sidenote: The final:ro
tells docker to mount it as read-only. This is just a precaution I like to use for files or folders where I want to make sure there is no way they're modified by the container. ) . - The PHP-FPM image for version 8.3. Here we are, again, mounting the current folder where
docker-compose.yml
lives on/var/www/app
inside the container. I’ve left, commented out, a couple of references in case we’d want to customize any PHP config, although we won’t be doing that for now. - The
composer
image we used for creating the Laravel project. I’ve only added a profile for it for the same reason.
Ok, we need to create the config file to tell nginx
we want to pass all PHP requests to PHP-FPM
and serve the project assets otherwise. I like to create a docker
folder inside the resources
folder to put any config file related to the local dev on there. You can call it whatever you want but make sure whatever folder you use, use the same path on the nginx
volume.
# resources/docker/vhost.conf
server {
listen 80;
server_name _;
index index.php index.html;
root /var/www/app/public;
location / {
try_files $uri /index.php?$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass fpm_app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
A pretty standard config file for PHP, if you need anything extra here like longer timeouts or bigger buffers it’s only a matter of adding whatever nginx directive you need. It’s worth mentioning that we can use fpm_app:9000
as the value passed to fastcgi_pass
because docker has its own internal DNS which will resolve the fpm_app
(the name we used on the docker-compose.yml
for the FPM image) to whatever IP docker assigned it in our system. The document_root
points to the public
folder within the project, which is the entrypoint for any Laravel app.
Now, to run the whole thing:
docker compose --profile web up -d
If you open your domain you’ll probably see a permission error. That is because FPM is running as www-data
on the container but the storage
and database
folders don’t have writing permissions for it. You can jump into the container to see what id has the www-data
group there. First, let’s check the container id that FPM got assigned:
docker ps --filter "name=fpm_app"
Which will give you something like this (your container id will be different):
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
656239cc397a php:8.3-fpm-alpine "docker-php-entrypoi…" 16 minutes ago Up 16 minutes 9000/tcp test-app2-fpm_app-1
With that ID, we can get into the running container with:
docker exec -it 656239cc397a sh
We’ll get a shell in the running container, now, we can do:
id www-data
Which will return:
uid=82(www-data) gid=82(www-data) groups=82(www-data),82(www-data)
With that info, we can run, on our local filesystem:
chgrp 82 -R storage && \
chgrp 82 database/database.sqlite && \
chmod g+w -R storage && \
chmod g+w database/database.sqlite
Now www-data
will have writing permissions on the container on both, the whole storage
folder and the default SQLite database. Reloading your page now should give you the welcome Laravel page.
Summary#
I’ve tried to demonstrate the whole process to get the up running on a computer without any software installed. As you’ve seen, docker and its commands are cumbersome and hard to remember. As demonstrated in this post, this is cool but not really handy. In the following post I’ll try to put together a few tricks and goodies I use to make this process actually nice to work with. I like to think of this post as the basis where all concepts are layed out but don’t be scared by it.