Getting Started with the Laravel Chainguard Image

Tutorial on how to get started with the Laravel Chainguard Image

The Laravel Chainguard Image is a container image that has the tooling necessary to develop, build, and execute Laravel applications, including required extensions. Laravel is a full-stack PHP framework that enables developers to build complex applications using modern tools and techniques that help streamline the development process.

In this guide, we’ll set up a demo and demonstrate how you can use Chainguard Images to develop, build, and run Laravel applications.

This tutorial requires Docker to be installed on your local machine. If you don’t have Docker installed, you can download and install it from the official Docker website.

1. Setting Up a Demo Application

We’ll start by getting the demo application ready. The demo is called OctoFacts, and it shows a random fact about Octopuses alongside a random Octopus image each time the page is reloaded. Quotes are loaded from a .txt file into the database through a database migration.

By default, the application uses an SQLite database. This allows us to test the application with the built-in web server provided by the artisan serve command, without having to set up a full PHP development environment first. In the next step, we’ll configure a multi-node environment using Docker Compose to demonstrate a typical LEMP (Linux, (E)Nginx, MariaDB, and PHP) environment using Chainguard Images.

Start by cloning the demos repository to your local machine:

git clone git@github.com:chainguard-dev/edu-images-demos.git

Locate the octo-facts demo and cd into its directory:

cd edu-images-demos/php/octo-facts

The demo includes a .env.dev file with the app’s configuration for development. You should create a copy of this file and save it as .env, so that the application can load default settings:

cp .env.dev .env

You can now use the builder Laravel image to install dependencies via Composer. Notice that we’re using the laravel system user in order to be able to write to the shared folder without issues:

docker run --rm -v ${PWD}:/app --entrypoint composer --user laravel \
    cgr.dev/chainguard/laravel:latest-dev \
    install

You can now run the database migrations and seed the database with sample data. This will populate a .sqlite database with facts obtained from the octofacts.txt file in the root of the application.

docker run --rm -v ${PWD}:/app --entrypoint php --user laravel \
    cgr.dev/chainguard/laravel:latest-dev \
    /app/artisan migrate --seed

Next, run npm install to install Node dependencies. You can use the node:latest-dev image for that, but you’ll need to use the root container user in order to be able to write to the shared folder using this image:

docker run --rm -v ${PWD}:/app --entrypoint npm --user root \
    cgr.dev/chainguard/node:latest-dev \
    install

Then, fix permissions on node modules with:

sudo chown -R ${USER} node_modules/

The next step is to build front end assets. You can use the npm run build command for that. Like with npm install, you’ll need to set the container user to root.

docker run --rm -v ${PWD}:/app --entrypoint npm --user root \
    cgr.dev/chainguard/node:latest-dev \
    run build

Fix permissions on the public folder:

sudo chown -R ${USER} public/

The application is all set. Next, run the built-in web server with:

docker run -p 8000:8000 --rm -it -v ${PWD}:/app \
  --entrypoint /app/artisan --user laravel \
  cgr.dev/chainguard/laravel:latest-dev serve --host=0.0.0.0

This command will create a port redirect that allows you to access the built-in web server running in the container. It uses a volume share for the application files, and sets the entrypoint to the artisan serve command.

You should now be able to access the application from your browser at localhost:8000. You’ll get a page similar to this:

Preview of the OctoFacts demo Laravel application

2. Creating a LEMP Environment with Docker Compose

To demonstrate a full LEMP setup using Chainguard Images, we’ll now set up a Docker Compose environment to serve the application via Nginx. This setup can be used as a more robust development environment that replicates a production setting based on secure container images.

The following docker-compose.yaml file is already included in the root of the application folder:

services:
  app:
    image: cgr.dev/chainguard/laravel:latest-dev
    restart: unless-stopped
    working_dir: /app
    volumes:
      - .:/app
    networks:
      - wolfi

  nginx:
    image: cgr.dev/chainguard/nginx
    restart: unless-stopped
    ports:
      - 8000:8080
    volumes:
      - .:/app
      - ./nginx.conf:/etc/nginx/nginx.conf
    networks:
      - wolfi

  mariadb:
    image: cgr.dev/chainguard/mariadb
    restart: unless-stopped
    environment:
      MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
      MARIADB_USER: laravel
      MARIADB_PASSWORD: password
      MARIADB_DATABASE: octofacts
    ports:
      - 3306:3306
    networks:
      - wolfi

networks:
  wolfi:
    driver: bridge

This docker-compose.yaml file defines 3 services to run the application (app, nginx, mariadb), using volumes to share the application files within the container and a configuration file for the Nginx server, which we’ll showcase in a moment. Notice the database credentials within the mariadb service: these environment variables are used to set up the database. This is done automatically by the MariaDB image entrypoint upon container initialization. We’ll use these credentials to configure the new database within Laravel’s .env file.

Please note this Docker Compose setup is intended for local development only. For production environments, you should never keep sensitive data like database credentials in plain text. Check the Docker Compose Documentation for more information on how to handle sensitive data in Compose files.

The following nginx.conf file is also included within the root of the application folder. This file is based on the recommended Nginx deployment configuration from official Laravel docs.

pid /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {
    server {
    listen 8080;
    root /app/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

    location / {
        include  /etc/nginx/mime.types;
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 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;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
  }
}

This file sets up the document root to app/public and configures Nginx to redirect .php requests to the container running PHP-FPM (app:9000).

You can bring the environment up with:

docker compose up

This command will block your terminal and show live logs from each of the running services. With the MariaDB database up and running, you can now update Laravel’s main .env file to change the database connection from sqlite to mysql.

Open the .env file in your editor of choice and locate the database settings. The DB_CONNECTION parameter is set to sqlite, you should change that to mysql and uncomment the remaining variables to reflect the settings from your docker-compose.yaml file. This is how the database section should look like when you’re finished editing:

DB_CONNECTION=mysql
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=octofacts
DB_USERNAME=laravel
DB_PASSWORD=password

Save and close the file. If you reload the application on your browser now, you should get a database error, because the database is empty. You’ll need to re-run migrations and seed the database. To do that, you can run a docker exec command on the live container.

First, look for the container running the app service and copy its name.

docker compose ps
NAME                   IMAGE                                   COMMAND                  SERVICE   CREATED          STATUS          PORTS
octo-facts-app-1       cgr.dev/chainguard/laravel:latest-dev   "/bin/s6-svscan /sv"     app       11 seconds ago   Up 10 seconds
octo-facts-mariadb-1   cgr.dev/chainguard/mariadb              "/usr/local/bin/dock…"   mariadb   11 seconds ago   Up 10 seconds   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp
octo-facts-nginx-1     cgr.dev/chainguard/nginx                "/usr/sbin/nginx -c …"   nginx     11 seconds ago   Up 10 seconds   0.0.0.0:8000->8080/tcp, :::8000->8080/tcp

Then, you can run migrations like this:

docker exec octo-facts-app-1 php /app/artisan migrate --seed

You can use the same method to execute other Artisan commands while the environment is up. After running migrations and seeding the database, you should be able to reload the app from your browser at localhost:8000 and get a new octopus fact.

3. Creating a Distroless Laravel Runtime for the Application

So far, we have been using the laravel:latest-dev builder image to run the application in a development setting. For production workloads, the recommended approach for additional security is to create a distroless runtime for the application that will contain only what’s absolutely necessary for running the app on production. This is done by combining a build phase in a multi-stage Dockerfile.

To demonstrate this approach, we’ll now build a distroless image and test it using the Docker Compose setup exemplified in the previous section.

The following Dockerfile is included within the root of the application:

FROM cgr.dev/chainguard/laravel:latest-dev AS builder
USER root
RUN apk update && apk add nodejs npm
COPY . /app
RUN cd /app && chown -R php.php /app
USER php
RUN composer install --no-progress --no-dev --prefer-dist
RUN npm install && npm run build

FROM cgr.dev/chainguard/laravel:latest
COPY --from=builder /app /app

This Dockerfile starts with a build stage that copies the application files to the container, installs Node and NPM, installs the application dependencies, and dumps front-end assets to the public folder. A second stage based on laravel:latest copies the application from the build stage to the final distroless image. It’s important to notice that we don’t run any database migrations here, because at build time the database might not be ready yet. In other production scenarios, you may be able to include the migration within the Dockerfile, as long as the database is already up and running.

The file docker-compose-distroless.yaml included within the root of the application folder has a few changes compared to the previous docker-compose.yaml file. The app service now uses the octofacts image, which is built from the Dockerfile referenced above. The user: laravel directive is also removed so commands run as the default php user. This is how the app service looks like in the new file:

  app:
    image: octofacts
    build:
      context: .
    restart: unless-stopped
    working_dir: /app
    user: laravel
    volumes:
      - .:/app
    networks:
      - wolfi
...

If your environment is still running, you should stop it before proceeding. Hit CTRL+C and then run:

docker compose down

This will stop and remove all containers, networks, and volumes created by the previous docker-compose.yaml file. You can now bring the new environment up with:

docker compose -f docker-compose-distroless.yaml up

Once the environment is up and running, you can now run the database migrations with:

docker exec octo-facts-app-1  php /app/artisan migrate --seed

You should now be able to reload your browser and obtain a new octopus fact.

Advanced Usage

If your project requires a more specific set of packages that aren't included within the general-purpose Laravel Chainguard Image, you'll first need to check if the package you want is already available on the wolfi-os repository.

Note: If you're building on top of an image other than the wolfi-base image, the image will run as a non-root user. Because of this, if you need to install packages with apk install you need to use the USER root directive.

If the package is available, you can use the wolfi-base image in a Dockerfile and install what you need with apk, then use the resulting image as base for your app. Check the "Using the wolfi-base Image" section of our images quickstart guide for more information.

If the packages you need are not available, you can build your own apks using melange. Please refer to this guide for more information.

Last updated: 2024-05-17 11:07