Getting Started with the PHP Chainguard Image

Tutorial on how to get started with the PHP Chainguard Image

The PHP images maintained by Chainguard include both development and production container images, suitable for building and running PHP workloads. The latest-fpm variant serves PHP applications over FastCGI, while the latest variant runs PHP applications from the command line.

In this guide, we’ll set up a demo and demonstrate how you can use Chainguard Images to develop, build, and run PHP 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.

What is distroless? Distroless images are minimalist container images containing only essential software required to build or execute an application. That means no package manager, no shell, and no bloat from software that only makes sense on bare metal servers.
What is Wolfi? Wolfi is a community Linux undistro created specifically for containers. This brings distroless to a new level, including additional features targeted at securing the software supply chain of your application environment: comprehensive SBOMs, signatures, daily updates, and timely CVE fixes.
Chainguard Images Chainguard Images are a mix of distroless and development images based on Wolfi. Nightly builds make sure images are up-to-date with the latest package versions and patches from upstream Wolfi.

1. Setting up a (CLI) Demo Application

We’ll start by getting the demo application ready. This CLI app generates random names based on a list of nouns and adjectives. To exemplify usage with Composer, the app has a single dependency on minicli, a minimalist CLI framework for PHP.

Start by cloning the demos repository to your local machine:

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

Locate the namegen demo and cd into its directory:

cd edu-images-demos/php/namegen

You can use the php:latest-dev image variant with a volume in order to install application dependencies with Composer. We’ll use the root user to be able to write to the volume mounted in the container:

docker run --rm -v ${PWD}:/app --entrypoint composer --user root \
    cgr.dev/chainguard/php:latest-dev \
    install --no-progress --no-dev --prefer-dist

You’ll get output like this, indicating that the package minicli/minicli was installed:

Installing dependencies from lock file
Verifying lock file contents can be installed on current platform.
Package operations: 1 install, 0 updates, 0 removals
  - Downloading minicli/minicli (4.2.0)
  - Installing minicli/minicli (4.2.0): Extracting archive
Generating autoload files
1 package you are using is looking for funding.
Use the `composer fund` command to find out more!

Next, make sure permissions are set correctly on the generated files.

On Linux systems run the following:

sudo chown -R ${USER}:${USER} .

On macOS systems, run this:

sudo chown -R ${USER} .

The application should now be ready to be executed. For transparency, here is the code that will be executed, which you’ll find in the namegen script:

#!/usr/bin/php
<?php

require __DIR__ . '/vendor/autoload.php';

use Minicli\App;

$app = new App();

$app->registerCommand('get', function () use ($app) {

    $animals = [ 'turtle', 'seagull', 'octopus', 'shark', 'whale', 'dolphin', 'walrus', 'penguin', 'seahorse'];
    $adjectives = [ 'ludicrous', 'mischievous', 'graceful', 'fortuitous', 'charming', 'ravishing', 'gregarious'];

    $app->getPrinter()->info($adjectives[array_rand($adjectives)] . '-' . $animals[array_rand($animals)]);
});

$app->runCommand($argv);

You can now execute the app to test it out. Run the following command:

docker run --rm -v ${PWD}:/work \
    cgr.dev/chainguard/php:latest \
    /work/namegen get

The command should output a random name combination:

ludicrous-walrus

In the next step, you’ll build the application in a multi-stage Dockerfile.

2. Building a Distroless Image for the Application

We’ll now build a distroless image for the application. To be able to install dependencies with Composer, our build will consist of two stages. First, we’ll build the application using the dev image variant, a Wolfi-based image that includes Composer and other useful tools for development. Then, we’ll create a separate stage for the final image. The resulting image will be based on the distroless PHP Wolfi image, which means it doesn’t come with Composer or even a shell.

For reference, here is the content of the included Dockerfile:

FROM cgr.dev/chainguard/php:latest-dev AS builder
USER root
COPY . /app
RUN chown -R php /app
USER php
RUN cd /app && \
    composer install --no-progress --no-dev --prefer-dist

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

ENTRYPOINT [ "php", "/app/namegen" ]

This Dockerfile will:

  1. Start a new build stage based on the php:latest-dev image and call it builder;
  2. Copy files from the current directory to the /app location in the container;
  3. Enter the /app directory and run composer install to install any dependencies;
  4. Start a new build stage based on the php:latest image;
  5. Copy the application from the builder stage;
  6. Set up the application as entry point for this image.

You can now build the image with:

docker build . -t php-namegen

You’ll get output similar to this:

[+] Building 0.1s (12/12) FINISHED                                                                        docker:default
 => [internal] load build definition from Dockerfile                                                                0.0s
 => => transferring dockerfile: 322B                                                                                0.0s
 => [internal] load metadata for cgr.dev/chainguard/php:latest-dev                                                  0.0s
 => [internal] load metadata for cgr.dev/chainguard/php:latest                                                      0.0s
 => [internal] load .dockerignore                                                                                   0.0s
 => => transferring context: 2B                                                                                     0.0s
 => [internal] load build context                                                                                   0.0s
 => => transferring context: 4.86kB                                                                                 0.0s
 => [builder 1/4] FROM cgr.dev/chainguard/php:latest-dev                                                            0.0s
 => [stage-1 1/2] FROM cgr.dev/chainguard/php:latest                                                                0.0s
 => CACHED [builder 2/4] COPY . /app                                                                                0.0s
 => CACHED [builder 3/4] RUN chown -R php /app                                                                      0.0s
 => CACHED [builder 4/4] RUN cd /app &&     composer install --no-progress --no-dev --prefer-dist                   0.0s
 => CACHED [stage-1 2/2] COPY --from=builder /app /app                                                              0.0s
 => exporting to image                                                                                              0.0s
 => => exporting layers                                                                                             0.0s
 => => writing image sha256:e617d7afd472d4a78d82060eaacd3a1c33310d6a267f6aaf9aa34b44e3ef8e5c                        0.0s
 => => naming to docker.io/library/php-namegen                                                                      0.0s

Once the build is finished, run the image with:

docker run --rm php-namegen get

And you should get output similar to what you got before, with a random name combination.

fortuitous-octopus

If you inspect the image with a docker image inspect php-namegen, you’ll notice that it has only two layers, thanks to the use of a multi-staging Docker build.

docker image inspect php-namegen
...
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:52cf795862535c5f22dac055428527508088becebbe00293457693a5f8fa1df2",
                "sha256:95a8dc6d81c92158ac032e5167768a04e45f25b3bf4009c5698673c19d36d5c2"
            ]
        },
        "Metadata": {
            "LastTagTime": "2024-11-15T12:52:18.412117879+01:00"
        }
    }
]

In such cases, the last FROM section from the Dockerfile is the one that composes the final image. That’s why in our case it only adds one layer on top of the base php:latest image, containing the COPY command we use to copy the application from the build stage to the final image.

It’s worth highlighting that nothing is carried from one stage to the other unless you copy it. That facilitates creating a slim final image with only what’s necessary to execute the application.

3. Working with the PHP-FPM Image Variant

The latest-fpm image variant is suitable for running PHP applications over FastCGI, to be served by a web server such as Nginx. In this section, we’ll run a Docker Compose setup using the latest-fpm image variant and the Chainguard Nginx image.

The namegen-api demo is a variation of the previous demo, but it serves the random name generation over HTTP. The application responds with a JSON payload containing the animal and adjective combination, and accepts an optional animal parameter to specify the animal for the final suggested name.

Start by accessing the namegen-api demo folder. This should be at the same level as the previous namegen demo in the edu-images-demos repository. If your terminal is still open on the previous demo, you can navigate to the namegen-api folder with:

cd ../namegen-api

The index.php file contains the following code:

<?php

$animals = [ 'turtle', 'seagull', 'octopus', 'shark', 'whale', 'dolphin', 'walrus', 'penguin', 'seahorse'];
$adjectives = [ 'ludicrous', 'mischievous', 'graceful', 'fortuitous', 'charming', 'ravishing', 'gregarious'];

$chosenAdjective = $adjectives[array_rand($adjectives)];
$chosenAnimal = $_GET['animal'] ?? $animals[array_rand($animals)];

echo json_encode(['animal' => $chosenAnimal, 'adjective' => $chosenAdjective]);

To serve this application over HTTP, we’ll use the latest-fpm image variant in a Docker Compose setup. The following docker-compose.yml is included within the namegen-api folder:

services:
  app:
    image: cgr.dev/chainguard/php:latest-fpm
    restart: unless-stopped
    working_dir: /app
    volumes:
      - ./:/app

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

This Docker Compose setup defines two services: app and nginx. The app service uses the latest-fpm image variant and mounts the current directory to the /app directory in the container. The nginx service uses the Chainguard Nginx image and also mounts the current directory to the /app directory in the container, setting it as the document root with a custom configuration. A second volume replaces the default Nginx configuration with our custom one, included as nginx.conf in the same directory:

events {
  worker_connections  1024;
}

http {
    server {
        listen 80;
        index index.php index.html;
        root /app/public;
        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 / {
            try_files $uri $uri/ /index.php?$query_string;
            gzip_static on;
        }
    }
}

To run this setup, execute:

docker-compose up

This command will start the services defined in the docker-compose.yml file. You can access the application at http://localhost:8000 in your browser, or you can make a curl request to it:

curl http://localhost:8000

You should get a JSON response with a random name combination:

{"animal":"octopus","adjective":"ludicrous"}

You can also try passing an animal parameter to the URL:

curl 'http://localhost:8000?animal=cat'
{"animal":"cat","adjective":"mischievous"}

To stop the services, you can press Ctrl+C in the terminal where you ran docker-compose up.

Advanced Usage

If your project requires a more specific set of packages that aren't included within the general-purpose PHP 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-11-15 11:07