Distributing a Laravel Zero application with Docker

Alexandre Gravel-Raymond
7 min readDec 2, 2017

--

I’ve been wanting to use the community project Laravel Zero at work since I learned about it. If you never heard about it, it’s a great PHP micro-framework created by Nuno Maduro, specifically designed for command line applications and based on Laravel components. Think of it as a supercharged Artisan but stripped of all the web-related logic that comes with a Laravel project.

So my plan was something like this:

  1. Start a new Laravel Zero project
  2. Deploy to production
  3. ????
  4. PROFIT!!!!

But something bugged me. I didn’t know how I could deploy my project to production. Zero has a strict dependency on PHP 7.1, and I couldn’t easily upgrade the PHP versions running on the production servers of the company I work for. Laravel Zero includes a command to easily package your application as a Phar (PHP Archive) file, but you still need the right PHP version to run it.

It bugged me for weeks, until I realized the solution was in fact really simple : containers ! Containers are lightweight systems that allow to package an application as well as all of its dependencies. It’s like distributing the execution environment at the same time as the code that is executed. So for my containerization quest I chose Docker, with which I have some experience and which has a very nice tool ecosystem.

The project

If you are already familiar with Laravel Zero, you can skip straight to the Package it! section. To start developing a Zero application, you still need to have PHP 7.1 available somehow on your local environment (directly installed on your system, or via a virtual machine provided with Vagrant, or of course with Docker). The only other prerequisite is Composer, a PHP package manager with which your are surely familiar if you develop with PHP.

Let’s start developing my application named “????” !

composer create-project --prefer-dist laravel-zero/laravel-zero ????

A fresh Zero app comes with a default “hello” command out of the box, but as its name suggests it’s mainly a salutation command, so let’s delete the app/Commands/HelloCommand.php file and add a new “profit” command:

cd ????
rm app/Commands/HelloCommand.php
php ???? make:command profit

I’ll then edit the contents of the new app/Commands/Profit.php. The first thing to do is to define the signature of the command, that defines the way it will be called, and its description, that will be shown when the application is called without argument. In my case I simply want to be able to call php ???? profit so let’s define it that way:

You may have noticed that my Profit command makes use of a database query with Laravel Query Builder. It’s not installed by default, but it’s very easy to enable it:

php ???? install:database

In my case, I want to get data from a local MySQL server, so I just need to change the contents of the app/config/database.php file with the proper configuration:

I test it by executing php ???? profit … and it works!

Lastly, I must not forget to set the “production” flag to false in the config/app.php file so that development commands included by default are disabled in the production environment. I can also disable the scheduler in the same file, as I don’t need it.

Package it!

Now that I’m completely satisfied with the current development status of my application, I will build a Docker image (think of an image as a base class, as opposed to containers as specific object instances) so that I can:

  1. Make a Sysadmin happy, and
  2. Let users who are afraid of PHP use my application without second thoughts.

The first step is to install Docker CE on my computer, and on all machines on which the application is expected to run. Having Docker CE installed allows both to build and run container-based applications.

Once that is done, building the image is rather simple. First, I drop a file named Dockerfile at the root of my project:

When choosing the image on which yours will be based, there’s a lot of possibilities. For PHP applications, I strongly suggest to use one of the official PHP images. In my case I chose the CLI version because I didn’t need Apache, and the Alpine distribution, which allows to have a lighter image.

In some cases (specifically when you don’t have any customization to do on the base image) you could completely skip the image building step and just use a bare PHP image to execute your application. In my case, the command to execute my application would look something like this (more on the arguments later):

docker run -it -rm --mount src="$(pwd)",target=/zero,type=bind --workdir /zero/ php:7.1-cli-alpine php ???? profit [1]

But in my case, it fails, because the PDO_MYSQL extension is not installed in the base image. Hence the second line of the Dockerfile. The docker-php-ext-install command is one of the scripts included in the official PHP images that help to customize your build, here by installing a PHP extension. You can also install additional packages by using the apk command provided by the Alpine base image.

The third line commits the project files in the image, so I don’t have to deploy/distribute the project files including the vendors via a separate channel. Finally, the fourth line defines the command that will be executed when one runs the container. The user will still be able to append arguments (such as the command name or options).

I’m ready to build the image by executing docker build -t ilesinge/questionmark:latest in my project’s root directory, where “ilesinge” is the username/namespace I chose for myself, “questionmark” is the repository name and “latest” is an additional tag which is used by default when someone pulls an image from a Docker registry. You may want to create an account on Docker Hub first to register your username, but you could still tag your image with a different name later on.

Run it!

Once the image is built, I can finally execute it with a container (locally):

docker run -it --rm --net=host --mount source=questionstorage,target=/zero/storage ilesinge/questionmark profit

I will alias this into a more convenient command, but first some explanations:

  • The -i flag allows the user to interact with the container, such as with the question asked by the $this->choice() method of my command.
  • The -t flag allocates a pseudo-TTY to the container, which notably enables colors in the commands output.
  • The --rm flag forces the container to be deleted after the command has ended
  • The --net=host option allows the container to access the host network. It is only useful in my case because I need the container to communicate with a local MySQL server.
  • The --mount source=questionstorage,target=/zero/storage part creates a persistent storage volume accross containers, named “questionstorage” and mounted at /zero/storage in the container, which maps to the default storage folder in a Laravel Zero application.

The last option is only needed if your command needs to locally store something on the filesystem that must be available in a future execution of the container. Note that the files stored on the mounted path will not be (easily) accessible from the host machine. In the case you need an access to the host filesystem, for input or output files for example, you can use a bind mount instead of a volume: --mount source=/tmp/question,target=/zero/storage,bind=mount. In that case, the source path must be an absolute path on the host filesystem and must already exist.

Note that there’s a feature of Laravel Zero that cannot be used without funny hacks from inside a container. It is the Desktop Notifications, because the notification tools inside the container don’t have access to the host’s OS.

Distribute it!

The simplest way to distribute a Docker image is by exporting it with docker save ilesinge/questionmark --output questionmark.tar, manually transferring and loading it with docker load --input /path/to/questionmark.tar on the destination host, but that can be a hassle on the long term, and doesn’t ease the process for end users.

So I want to use Docker Hub (or a self-hosted Docker registry) to store and distribute my images. The steps to use it are simple:

  1. Create an account on Docker Hub
  2. Create a repository for your project on Docker Hub (in my case, with the name “questionmark”
  3. Back in your terminal, execute docker login --username=<yourusername>
  4. Execute docker push <yourusername>/<yourrepositoryname> (in my case that would be ilesinge/questionmark)

And it’s done ! After that, the end user can simply execute (after ensuring Docker is installed on their system):

docker run (...) ilesinge/questionmark profit

And to make sure they have the latest version I published, they can pull the last image once in a while:

docker pull ilesinge/questionmark

It may be interesting to also provide the end users with a wrapper script that checks for the Docker dependency and hides the docker run parameter complexity without aliasing, but this wrapper script will need to be duplicated for each platform on which the application is going to be used (eg.: a batch script on Windows and a bash script on Linux/Mac).

Finally…

I‘m now ready to use Laravel Zero in production at work!

The next step would probably be to automate the image building process, for instance by using Docker Hub’s Automated Builds to build a new image as soon as I push on my GitHub repository.

If you want to test a very simple Laravel Zero application on your machine, you can use this demo container:

docker run -it --rm ilesinge/zero hello

And as the gifts season is beginning, you can now install Laravel Zero without installing Composer (or PHP at all) on your computer by using the (non-official) dockerized Laravel Zero installer:

docker run -it --rm --mount src="$(pwd)",target=/workspace,type=bind --user=$(id -u):$(id -g) ilesinge/zero-installer new <yourapplication> [1]

Thank you for reading !

  1. The $(pwd) and --user command parts assume you are on a Linux or Mac machine. For Windows users, $(pwd) should be replaced with %cd% and I frankly don’t know if the --user part (to fix rights on your new app folder) is useful.

--

--