Skip to content

Commit

Permalink
Updating PHP guide - maintenance sprint
Browse files Browse the repository at this point in the history
  • Loading branch information
erikaheidi committed Nov 15, 2024
1 parent a4a35a1 commit 9f0b8e7
Showing 1 changed file with 173 additions and 68 deletions.
241 changes: 173 additions & 68 deletions content/chainguard/chainguard-images/getting-started/php.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,61 @@ weight: 045
toc: true
---

The PHP images maintained by Chainguard are a mix of development and production distroless images that are suitable for building and running PHP workloads.
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.

Because PHP applications typically require the installation of third-party dependencies via Composer, using a pure distroless image for building your application would not work. In cases like this, you'll need to implement a [multi-stage Docker build](https://docs.docker.com/build/building/multi-stage/) that uses one of the `-dev` images to set up the application.
In this guide, we'll set up a demo and demonstrate how you can use Chainguard Images to develop, build, and run PHP applications.

In this guide, we'll set up a distroless container image based on Wolfi as a runtime to execute a command-line PHP application.
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/).

{{< details "What is distroless" >}}
{{< details "What is distroless?" >}}
{{< blurb/distroless >}}
{{< /details >}}

{{< details "What is Wolfi" >}}
{{< details "What is Wolfi?" >}}
{{< blurb/wolfi >}}
{{< /details >}}

{{< details "Chainguard Images" >}}
{{< blurb/images >}}
{{< /details >}}

## Step 1: Setting up a Demo Application
## 1. Setting up a (CLI) Demo Application

We'll start by creating a basic command-line PHP application to serve as a demo. This app will generate random names based on a list of nouns and adjectives. To exemplify usage with Composer, the app will have a single dependency on [minicli](https://github.com/minicli/minicli), a minimalist CLI framework for PHP.
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.

First, create a directory for your app. In this guide we'll use `wolfi-php`:
Start by cloning the demos repository to your local machine:

```shell
mkdir ~/wolfi-php && cd ~/wolfi-php
git clone git@github.com:chainguard-dev/edu-images-demos.git
```

If you have a local PHP development environment with Composer, you can run the following command to download the single dependency of this app.
Locate the `namegen` demo and cd into its directory:

```
composer require minicli/minicli
```shell
cd edu-images-demos/php/namegen
```

If you don't have a local PHP development environment, you can use the `php:latest-dev` image variant with a volume in order to install application dependencies with Composer:
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}:/work --entrypoint composer --user root \
docker run --rm -v ${PWD}:/app --entrypoint composer --user root \
cgr.dev/chainguard/php:latest-dev \
require minicli/minicli --working-dir=/work
install --no-progress --no-dev --prefer-dist
```

If you used the Docker method, make sure permissions are set correctly on the generated files.
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:

Expand All @@ -73,19 +85,7 @@ On macOS systems, run this:
sudo chown -R ${USER} .
```

Create a new file to serve as the application entry point. We'll call it `namegen`:

```shell
touch namegen
```

Next, open the file in your code editor of choice, for example with `nano`:

```shell
nano namegen
```

The following PHP script defines a minimalist CLI app with a single command called `get`. It returns a random name based on a list of nouns and a list of adjectives.
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
Expand All @@ -109,19 +109,7 @@ $app->runCommand($argv);

```

Copy this content to your `namegen` script, save and close the file. Next, make the script executable with:

```shell
chmod +x namegen
```

You can now execute the app to test it out. If you have `php` locally, run:

```shell
php namegen get
```

If you're using the Docker setup, run:
You can now execute the app to test it out. Run the following command:

```shell
docker run --rm -v ${PWD}:/work \
Expand All @@ -134,35 +122,18 @@ 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.

The demo application is now ready. In the next step, you'll create a Dockerfile to run your app.

## Step 2: Creating the Dockerfile
## 2. Building a Distroless Image for the Application

To make sure our final image is _distroless_ while still being 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.
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.

Create a Dockerfile with:

```shell
touch Dockerfile
```

Then open this file in your code editor of choice, for example `nano`:
Open the `Dockerfile` included within the root of the application folder. You can use your code editor of choice, for example `nano`:

```shell
nano Dockerfile
```
The following 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.

Copy this content to your own `Dockerfile`:
For convenience, here is the content of the included `Dockerfile`:

```Dockerfile
FROM cgr.dev/chainguard/php:latest-dev AS builder
Expand All @@ -178,14 +149,43 @@ COPY --from=builder /app /app

ENTRYPOINT [ "php", "/app/namegen" ]
```
Save the file when you're finished.
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:

```shell
docker build . -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 image with:

```shell
Expand All @@ -208,12 +208,12 @@ docker image inspect php-namegen
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:23a50695d43b8aea7720c05bff1bdbfbcb45d0b0c7e7387f55d82110084002eb",
"sha256:9b900cbd280a3d510588c3b14bc937718ccee43a10b8b7b1756438b030bc3e15"
"sha256:52cf795862535c5f22dac055428527508088becebbe00293457693a5f8fa1df2",
"sha256:95a8dc6d81c92158ac032e5167768a04e45f25b3bf4009c5698673c19d36d5c2"
]
},
"Metadata": {
"LastTagTime": "2023-01-10T19:02:13.062958609+01:00"
"LastTagTime": "2024-11-15T12:52:18.412117879+01:00"
}
}
]
Expand All @@ -223,6 +223,111 @@ In such cases, the last `FROM` section from the Dockerfile is the one that compo

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 server via 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:

```shell
cd edu-images-demos/php/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 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

{{< blurb/images-advanced image="PHP" >}}

0 comments on commit 9f0b8e7

Please sign in to comment.