Use containers for PHP development

Prerequisites

Complete Containerize a PHP application.

Overview

In this section, you'll learn how to set up a development environment for your containerized application. This includes:

  • Adding a local database and persisting data
  • Adding phpMyAdmin to interact with the database
  • Configuring Compose to automatically update your running Compose services as you edit and save your code
  • Creating a development container that contains the dev dependencies

Add a local database and persist data

You can use containers to set up local services, like a database. To do this for the sample application, you'll need to do the following:

  • Update the Dockerfile to install extensions to connect to the database
  • Update the compose.yaml file to add a database service and volume to persist data

Update the Dockerfile to install extensions

To install PHP extensions, you need to update the Dockerfile. Open your Dockerfile in an IDE or text editor and then update the contents. The following Dockerfile includes one new line that installs the pdo and pdo_mysql extensions. All comments have been removed.

# syntax=docker/dockerfile:1

FROM composer:lts as deps
WORKDIR /app
RUN --mount=type=bind,source=composer.json,target=composer.json \
    --mount=type=bind,source=composer.lock,target=composer.lock \
    --mount=type=cache,target=/tmp/cache \
    composer install --no-dev --no-interaction

FROM php:8.2-apache as final
RUN docker-php-ext-install pdo pdo_mysql
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY --from=deps app/vendor/ /var/www/html/vendor
COPY ./src /var/www/html
USER www-data

For more details about installing PHP extensions, see the Official Docker Image for PHP.

Update the compose.yaml file to add a db and persist data

Open the compose.yaml file in an IDE or text editor. You'll notice it already contains commented-out instructions for a PostgreSQL database and volume. For this application, you'll use MariaDB. For more details about MariaDB, see the MariaDB Official Docker image.

Open the src/database.php file in an IDE or text editor. You'll notice that it reads environment variables in order to connect to the database.

In the compose.yaml file, you'll need to update the following:

  1. Uncomment and update the database instructions for MariaDB.
  2. Add a secret to the server service to pass in the database password.
  3. Add the database connection environment variables to the server service.
  4. Uncomment the volume instructions to persist data.

The following is the updated compose.yaml file. All comments have been removed.

services:
  server:
    build:
      context: .
    ports:
      - 9000:80
    depends_on:
      db:
        condition: service_healthy
    secrets:
      - db-password
    environment:
      - PASSWORD_FILE_PATH=/run/secrets/db-password
      - DB_HOST=db
      - DB_NAME=example
      - DB_USER=root
  db:
    image: mariadb
    restart: always
    user: root
    secrets:
      - db-password
    volumes:
      - db-data:/var/lib/mysql
    environment:
      - MARIADB_ROOT_PASSWORD_FILE=/run/secrets/db-password
      - MARIADB_DATABASE=example
    expose:
      - 3306
    healthcheck:
      test:
        [
          "CMD",
          "/usr/local/bin/healthcheck.sh",
          "--su-mysql",
          "--connect",
          "--innodb_initialized",
        ]
      interval: 10s
      timeout: 5s
      retries: 5
volumes:
  db-data:
secrets:
  db-password:
    file: db/password.txt

Note

To learn more about the instructions in the Compose file, see Compose file reference.

Before you run the application using Compose, notice that this Compose file uses secrets and specifies a password.txt file to hold the database's password. You must create this file as it's not included in the source repository.

In the docker-php-sample directory, create a new directory named db and inside that directory create a file named password.txt. Open password.txt in an IDE or text editor and add the following password. The password must be on a single line, with no additional lines in the file.

example

Save and close the password.txt file.

You should now have the following in your docker-php-sample directory.

├── docker-php-sample/
│ ├── .git/
│ ├── db/
│ │ └── password.txt
│ ├── src/
│ ├── tests/
│ ├── .dockerignore
│ ├── .gitignore
│ ├── compose.yaml
│ ├── composer.json
│ ├── composer.lock
│ ├── Dockerfile
│ ├── README.Docker.md
│ └── README.md

Run the following command to start your application.

$ docker compose up --build

Open a browser and view the application at http://localhost:9000/database.php. You should see a simple web application with text and a counter that increments every time you refresh.

Press ctrl+c in the terminal to stop your application.

Verify that data persists in the database

In the terminal, run docker compose rm to remove your containers and then run docker compose up to run your application again.

$ docker compose rm
$ docker compose up --build

Refresh http://localhost:9000/database.php in your browser and verify that the previous count still exists. Without a volume, the database data wouldn't persist after you remove the container.

Press ctrl+c in the terminal to stop your application.

Add phpMyAdmin to interact with the database

You can easily add services to your application stack by updating the compose.yaml file.

Update your compose.yaml to add a new service for phpMyAdmin. For more details, see the phpMyAdmin Official Docker Image. The following is the updated compose.yaml file.

services:
  server:
    build:
      context: .
    ports:
      - 9000:80
    depends_on:
      db:
        condition: service_healthy
    secrets:
      - db-password
    environment:
      - PASSWORD_FILE_PATH=/run/secrets/db-password
      - DB_HOST=db
      - DB_NAME=example
      - DB_USER=root
  db:
    image: mariadb
    restart: always
    user: root
    secrets:
      - db-password
    volumes:
      - db-data:/var/lib/mysql
    environment:
      - MARIADB_ROOT_PASSWORD_FILE=/run/secrets/db-password
      - MARIADB_DATABASE=example
    expose:
      - 3306
    healthcheck:
      test:
        [
          "CMD",
          "/usr/local/bin/healthcheck.sh",
          "--su-mysql",
          "--connect",
          "--innodb_initialized",
        ]
      interval: 10s
      timeout: 5s
      retries: 5
  phpmyadmin:
    image: phpmyadmin
    ports:
      - 8080:80
    depends_on:
      - db
    environment:
      - PMA_HOST=db
volumes:
  db-data:
secrets:
  db-password:
    file: db/password.txt

In the terminal, run docker compose up to run your application again.

$ docker compose up --build

Open http://localhost:8080 in your browser to access phpMyAdmin. Log in using root as the username and example as the password. You can now interact with the database through phpMyAdmin.

Press ctrl+c in the terminal to stop your application.

Automatically update services

Use Compose Watch to automatically update your running Compose services as you edit and save your code. For more details about Compose Watch, see Use Compose Watch.

Open your compose.yaml file in an IDE or text editor and then add the Compose Watch instructions. The following is the updated compose.yaml file.

services:
  server:
    build:
      context: .
    ports:
      - 9000:80
    depends_on:
      db:
        condition: service_healthy
    secrets:
      - db-password
    environment:
      - PASSWORD_FILE_PATH=/run/secrets/db-password
      - DB_HOST=db
      - DB_NAME=example
      - DB_USER=root
    develop:
      watch:
        - action: sync
          path: ./src
          target: /var/www/html
  db:
    image: mariadb
    restart: always
    user: root
    secrets:
      - db-password
    volumes:
      - db-data:/var/lib/mysql
    environment:
      - MARIADB_ROOT_PASSWORD_FILE=/run/secrets/db-password
      - MARIADB_DATABASE=example
    expose:
      - 3306
    healthcheck:
      test:
        [
          "CMD",
          "/usr/local/bin/healthcheck.sh",
          "--su-mysql",
          "--connect",
          "--innodb_initialized",
        ]
      interval: 10s
      timeout: 5s
      retries: 5
  phpmyadmin:
    image: phpmyadmin
    ports:
      - 8080:80
    depends_on:
      - db
    environment:
      - PMA_HOST=db
volumes:
  db-data:
secrets:
  db-password:
    file: db/password.txt

Run the following command to run your application with Compose Watch.

$ docker compose watch

Open a browser and verify that the application is running at http://localhost:9000/hello.php.

Any changes to the application's source files on your local machine will now be immediately reflected in the running container.

Open hello.php in an IDE or text editor and update the string Hello, world! to Hello, Docker!.

Save the changes to hello.php and then wait a few seconds for the application to sync. Refresh http://localhost:9000/hello.php in your browser and verify that the updated text appears.

Press ctrl+c in the terminal to stop Compose Watch. Run docker compose down in the terminal to stop the application.

Create a development container

At this point, when you run your containerized application, Composer isn't installing the dev dependencies. While this small image is good for production, it lacks the tools and dependencies you may need when developing and it doesn't include the tests directory. You can use multi-stage builds to build stages for both development and production in the same Dockerfile. For more details, see Multi-stage builds.

In the Dockerfile, you'll need to update the following:

  1. Split the deps staged into two stages. One stage for production (prod-deps) and one stage (dev-deps) to install development dependencies.
  2. Create a common base stage.
  3. Create a new development stage for development.
  4. Update the final stage to copy dependencies from the new prod-deps stage.

The following is the Dockerfile before and after the changes.


# syntax=docker/dockerfile:1

FROM composer:lts as deps
WORKDIR /app
RUN --mount=type=bind,source=composer.json,target=composer.json \
    --mount=type=bind,source=composer.lock,target=composer.lock \
    --mount=type=cache,target=/tmp/cache \
    composer install --no-dev --no-interaction

FROM php:8.2-apache as final
RUN docker-php-ext-install pdo pdo_mysql
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY --from=deps app/vendor/ /var/www/html/vendor
COPY ./src /var/www/html
USER www-data
# syntax=docker/dockerfile:1

FROM composer:lts as prod-deps
WORKDIR /app
RUN --mount=type=bind,source=./composer.json,target=composer.json \
    --mount=type=bind,source=./composer.lock,target=composer.lock \
    --mount=type=cache,target=/tmp/cache \
    composer install --no-dev --no-interaction

FROM composer:lts as dev-deps
WORKDIR /app
RUN --mount=type=bind,source=./composer.json,target=composer.json \
    --mount=type=bind,source=./composer.lock,target=composer.lock \
    --mount=type=cache,target=/tmp/cache \
    composer install --no-interaction

FROM php:8.2-apache as base
RUN docker-php-ext-install pdo pdo_mysql
COPY ./src /var/www/html

FROM base as development
COPY ./tests /var/www/html/tests
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
COPY --from=dev-deps app/vendor/ /var/www/html/vendor

FROM base as final
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY --from=prod-deps app/vendor/ /var/www/html/vendor
USER www-data

Update your compose.yaml file by adding an instruction to target the development stage.

The following is the updated section of the compose.yaml file.

services:
  server:
    build:
      context: .
      target: development
      # ...

Your containerized application will now install the dev dependencies.

Run the following command to start your application.

$ docker compose up --build

Open a browser and view the application at http://localhost:9000/hello.php. You should still see the simple "Hello, Docker!" application.

Press ctrl+c in the terminal to stop your application.

While the application appears the same, you can now make use of the dev dependencies. Continue to the next section to learn how you can run tests using Docker.

Summary

In this section, you took a look at setting up your Compose file to add a local database and persist data. You also learned how to use Compose Watch to automatically sync your application when you update your code. And finally, you learned how to create a development container that contains the dependencies needed for development.

Related information:

Next steps

In the next section, you'll learn how to run unit tests using Docker.