# Getting started with the PHP Chainguard Container

URL: https://chainguard-docs-preview-git-fix-learning-lab-jsonld.chainguard.app/chainguard/containers/getting-started/languages-and-runtimes/php.md
Last Modified: September 28, 2026
Tags: Chainguard Containers

Learn how to use Chainguard's PHP container images for secure web applications, featuring FPM and CLI variants with minimal vulnerabilities and reduced attack surface


Chainguard's PHP container images provide secure foundations for PHP applications with minimal vulnerabilities compared to traditional PHP images. These images come in multiple variants: the `latest-fpm` variant for serving web applications via FastCGI, the `latest` variant for CLI applications, and development variants that include additional tools for building and debugging PHP workloads.

In this guide, we'll set up a demo and demonstrate how you can use Chainguard Containers 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](https://docs.docker.com/get-docker/).

**What is distroless?**


Distroless container images are minimal 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](https://github.com/wolfi-dev/) 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.




**What are multi-stage builds?**


<p><a href="https://docs.docker.com/build/building/multi-stage/">Multi-stage builds</a> are a Docker feature that allow you to use multiple <code>FROM</code> statements in a single Dockerfile, where each statement begins a new build stage. In a typical pattern, an early stage uses a full-featured builder image to compile code or generate artifacts, while a later stage uses a minimal runtime image and copies in only what's needed to run the application. Only what you explicitly copy from one stage carries forward — everything else is discarded when that stage completes.</p>

<p>This approach has significant security benefits. Build tools like compilers, shells, and package managers are broadly exploitable general-purpose utilities that expand an image's attack surface. By leaving them behind in the builder stage, the runtime image has fewer packages, fewer potential CVEs, and a smaller blast radius in the event of a compromise. Reducing unnecessary components also improves observability and makes risk assessment easier, since every package in the final image can be directly tied to a runtime requirement.</p>

<p>Chainguard's container images are designed with this pattern in mind. Most have a <a href="/chainguard/containers/concepts/container-variants/"><code>:latest-dev</code> development variant</a> suited for use as a builder stage, and a corresponding <code>:latest</code> (or <code>-slim</code>) standard image for the distroless runtime. For example, a Go application can be compiled in the <code>go:latest-dev</code> builder stage and its binary copied into a <code>static</code> or <code>glibc-dynamic</code> runtime image — with no Go toolchain in the final container.</p>




**Chainguard Containers**


[Chainguard Containers](https://www.chainguard.dev/chainguard-images?utm_source=cg-academy&utm_medium=referral&utm_campaign=dev-enablement) is a mix of distroless and development container images based on Wolfi. Daily 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](https://github.com/minicli/minicli), a minimalist CLI framework for PHP.

Start by cloning the demos repository to your local machine:

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

Locate the `namegen` demo and cd into its directory:

```shell
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:

```shell
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:

```shell
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:

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

On macOS systems, run this:

```shell
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:

```php
#!/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:

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

The command should output a random name combination:

```shell
ludicrous-walrus
```

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

## 2. Building a distroless container for the application

We'll now build a distroless container 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 development variant, a Wolfi-based image that includes Composer and other useful tools for development. Then, we'll create a separate stage for the final container. The resulting container 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`:

```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` container 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 container.

You can now build the container with:

```shell
docker build . --pull -t php-namegen
```

You'll get output similar to this:

```shell
[+] 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 container with:

```shell
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 container 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.

```shell
docker image inspect php-namegen
```

```shell
...
        "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 container.

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:

```shell
cd ../namegen-api
```

The `index.php` file contains the following code:

```php
<?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:

```yaml
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 container 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:

```shell
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:

```shell
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:

```shell
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

<p>If your project requires a more specific set of packages that aren't included within the general-purpose PHP Chainguard Container, you'll first need to check if the package you want is already available on the <a href="https://github.com/wolfi-dev/os" title="Wolfi on GitHub">wolfi-os repository</a>. </p>

<blockquote>
    <p><b>Note</b>: If you're building on top of a container image other than the wolfi-base container image, the image will run as a non-root user. Because of this, if you need to install packages with <code>apk add</code> you need to use the <code>USER root</code> directive.</p>
</blockquote>

<p>If the package is available, you can use the <a href="https://images.chainguard.dev/directory/image/wolfi-base/overview" title="Overview of wolfi-base image">wolfi-base</a> image in a Dockerfile and install what you need with <code>apk</code>, then use the resulting image as the base for your app.
For a worked example, see the <a href="/chainguard/containers/using-and-deploying/using-containers/#using-the-wolfi-base-container-image-within-dockerfiles">Using the wolfi-base container image within Dockerfiles</a> section of our <a href="/chainguard/containers/using-and-deploying/using-containers/" title="How to use Chainguard Containers">guide to using Chainguard Containers</a>.
</p>

<p>If the packages you need are not available, you can build your own apks using <a href="/open-source/build-tools/melange/">melange</a>. To get started, see <a href="/open-source/build-tools/melange/getting-started-with-melange/">Getting started with melange</a>.</p>


