# Getting Started with the WordPress Chainguard Container

URL: https://edu.chainguard.dev/chainguard/chainguard-images/getting-started/wordpress.md
Last Modified: July 23, 2025
Tags: Chainguard Containers

Learn how to deploy WordPress using Chainguard's security-hardened container image with reduced vulnerabilities and distroless runtime options

Chainguard&rsquo;s WordPress container image provides a security-hardened foundation for WordPress deployments with significantly fewer vulnerabilities than traditional WordPress images. Designed as a drop-in replacement for the official WordPress FPM-Alpine image, this container includes a distroless variant that enhances production security by removing unnecessary system components. Built with the latest PHP and WordPress versions, it includes all required extensions while maintaining a minimal attack surface.
In this guide, we&rsquo;ll demonstrate 3 different ways in which you can use the WordPress Chainguard Container to build and run WordPress projects.
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 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?Multi-stage builds are a Docker feature that allow you to use multiple FROM 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.
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.
Chainguard Containers are designed with this pattern in mind. Most have a :latest-dev development variant suited for use as a builder stage, and a corresponding :latest (or -slim) standard image for the distroless runtime. For example, a Go application can be compiled in the go:latest-dev builder stage and its binary copied into a static or glibc-dynamic runtime image — with no Go toolchain in the final container.
Chainguard ContainersChainguard Containers are 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. Preparation This tutorial requires Docker to be installed on your local machine. If you don&rsquo;t have Docker installed, you can download and install it from the official Docker website.
Cloning the Demos Repository Start by cloning the demos repository to your local machine:
git clone git@github.com:chainguard-dev/edu-images-demos.gitLocate the wordpress demo and cd into its directory:
cd edu-images-demos/php/wordpressHere you will find three folders, each with a different demo that we&rsquo;ll cover in this guide.
Example 1: Testing the Container Image with a Fresh WordPress Install You can use the latest-dev variant of the Chainguard WordPress Container to create a project from scratch and go through the installation wizard. This method is useful for testing the container image and getting familiar with its features, however, changes made to the WordPress installation will not persist unless you set up a volume with proper permissions to share container contents with the host machine. We&rsquo;ll see how to do that in the next example.
The files for this demo are located in the 01-preview directory. You can access this directory and open the docker-compose.yaml file in your editor of choice to follow along.
Here&rsquo;s the content of the docker-compose.yaml file from our first demo:
services: app: image: cgr.dev/chainguard/wordpress:latest-dev restart: unless-stopped environment: WORDPRESS_DB_HOST: mariadb WORDPRESS_DB_USER: $WORDPRESS_DB_USER WORDPRESS_DB_PASSWORD: $WORDPRESS_DB_PASSWORD WORDPRESS_DB_NAME: $WORDPRESS_DB_NAME volumes: - document-root:/var/www/html nginx: image: cgr.dev/chainguard/nginx restart: unless-stopped ports: - 8000:8080 volumes: - document-root:/var/www/html - ./nginx.conf:/etc/nginx/nginx.conf mariadb: image: cgr.dev/chainguard/mariadb restart: unless-stopped environment: MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1 MARIADB_USER: $WORDPRESS_DB_USER MARIADB_PASSWORD: $WORDPRESS_DB_PASSWORD MARIADB_DATABASE: $WORDPRESS_DB_NAME ports: - 3306:3306 volumes: document-root:In this Docker Compose example, we define 3 services: app, nginx, and mariadb. Here&rsquo;s a breakdown of each service:
The app service uses the latest-dev variant of the Chainguard WordPress Container, and is configured to connect to the mariadb service. The entrypoint script in the WordPress container image looks for environment variables to set up a custom wp-config.php file. The volume document-root defines a volume that will be shared between the app and the nginx services. The nginx service uses the Chainguard nginx Container, and is configured to serve the WordPress application on port 8000. The mariadb service uses the Chainguard MariaDB Container, and is configured with the necessary environment variables to create a database for the WordPress application. The environment variables used in this example are defined in a .env file located in the same directory as the docker-compose.yaml file. To check for its contents, run:
cat .envWORDPRESS_DB_HOST=mariadb WORDPRESS_DB_USER=wp-user WORDPRESS_DB_PASSWORD=wp-password WORDPRESS_DB_NAME=wordpressAlthough not necessary, you can change these values to suit your needs. Notice this is a hidden file and might not be visible in your file explorer, but you can open it in your terminal using a text editor like nano or vim.
To start the services, run:
docker compose upIf you navigate to http://localhost:8000 in your browser, you should access the WordPress installation page. Follow the on-screen instructions to complete the WordPress setup. Keep in mind that any customizations will be lost once the environment is turned down.
To stop the services, type CTRL+C in the terminal where the services are running, and then run:
docker compose downThis will remove the containers and networks created by the docker compose up command. In the next example, we&rsquo;ll demonstrate how you can set up a volume with proper permissions to be able to persist customizations such as themes and plugins.
Example 2: Customizing a New WordPress Installation To persist customizations made to your WordPress site, such as the installation of new themes and plugins, you&rsquo;ll need to set up a volume with proper permissions in order to keep data between container rebuilds. This requires having a system user in the container with the same UID as your local system user on the host machine. To set this up, we&rsquo;ll create a custom Dockerfile that adds a wordpress user with the specified UID (set to 1000 by default, which is typically the UID of a regular user on Linux-based systems) to the latest-dev variant of the Chainguard WordPress Container. The Dockerfile also changes default permissions on the /var/www/html directory to allow the wordpress user to write to it.
Navigate to the 02-customizing directory to follow along. This is how the described Dockerfile included in this directory looks:
FROM cgr.dev/chainguard/wordpress:latest-dev ARG UID=1000 USER root RUN addgroup wordpress &amp;&amp; adduser -SD -u &#34;$UID&#34; -s /bin/bash wordpress wordpress RUN chown -R wordpress:wordpress /var/www/html USER wordpressIn the docker-compose.yaml file, we&rsquo;ll reference the custom Dockerfile and pass the UID as a build argument:
services: app: image: wordpress-local-dev build: context: . dockerfile: Dockerfile args: UID: 1000 user: wordpress restart: unless-stopped environment: WORDPRESS_DB_HOST: mariadb WORDPRESS_DB_USER: $WORDPRESS_DB_USER WORDPRESS_DB_PASSWORD: $WORDPRESS_DB_PASSWORD WORDPRESS_DB_NAME: $WORDPRESS_DB_NAME volumes: - ./wp-content:/var/www/html/wp-content - document-root:/var/www/html nginx: image: cgr.dev/chainguard/nginx restart: unless-stopped ports: - 8000:8080 volumes: - document-root:/var/www/html - ./nginx.conf:/etc/nginx/nginx.conf mariadb: image: cgr.dev/chainguard/mariadb restart: unless-stopped environment: MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1 MARIADB_USER: $WORDPRESS_DB_USER MARIADB_PASSWORD: $WORDPRESS_DB_PASSWORD MARIADB_DATABASE: $WORDPRESS_DB_NAME ports: - 3306:3306 volumes: document-root:Only the app service has changed in this example. We&rsquo;ve added a build section that references the custom Dockerfile, and we&rsquo;ve set the UID build argument to 1000 by default. This can be overwritten at runtime when you call docker compose up. We&rsquo;ve also added a volume share to persist the contents of the wp-content folder to the host machine.
To build your custom container image and pass along your own UID as build argument, run:
docker compose build --build-arg UID=$(id -u) appYou should get output indicating that the container image was successfully built. Now you can get your environment up with:
docker compose upOnce the environment is up and running, you can access your WordPress installation from your browser at localhost:8000.
If you go to another terminal window and check the contents of the 02-customizing/wp-content folder, you&rsquo;ll notice that it was populated with the default WordPress themes and plugins:
❯ ls -la wp-content total 24 drwxrwxr-x 4 erika erika 4096 Jul 18 21:16 . drwxrwxr-x 3 erika erika 4096 Jul 18 21:15 .. -rw-rw-r-- 1 erika erika 14 Jul 18 21:05 .gitignore -rw-r--r-- 1 erika 65533 28 Jan 1 1970 index.php drwxr-xr-x 2 erika 65533 4096 Jul 18 21:16 plugins drwxr-xr-x 16 erika 65533 4096 Jul 18 21:16 themesThis is only possible because of the custom Dockerfile we created, which added a wordpress user with the same UID as the local user on the host machine.
You can now install new themes and plugins, and they will persist between container rebuilds.
To stop the services, type CTRL+C in the terminal where the services are running, and then run:
docker compose downIn the next example, we&rsquo;ll see how you can create a distroless WordPress runtime for your production environment.
Example 3: Using the Distroless Variant of the WordPress Container Image This demo uses a multi-stage Docker build to create a final distroless container image to improve overall security. The distroless image contains the necessary dependencies to run WordPress and won&rsquo;t allow for new package installations or shell access, reducing the image attack surface.
The main difference here is that we&rsquo;re calling the entrypoint script at build time instead of run time. This is done to ensure the image is self-contained and doesn&rsquo;t rely on volumes set up within the host machine in order to work. Any customizations should be included in the wp-content folder that will be copied to the image at build time. Although this increases final image size due to the inclusion of custom content at build time, it limits what can be changed or added to the container image once it&rsquo;s built.
This demo includes a theme (Cue, a simple blogging theme) and a plugin (Imsanity, a popular plugin used to resize images) to demonstrate how to include custom content in the container image.
Navigate to the 03-distroless directory to follow along. This is what the Dockerfile included in this directory looks like:
FROM cgr.dev/chainguard/wordpress:latest-dev AS builder #trigger wp-config.php creation ENV WORDPRESS_DB_HOST=foo #copy wp-content folder COPY ./wp-content /usr/src/wordpress/wp-content #run entrypoint script RUN /usr/local/bin/docker-entrypoint.sh php-fpm --version FROM cgr.dev/chainguard/wordpress:latest COPY --from=builder --chown=php:php /var/www/html /var/www/htmlNotice that we&rsquo;re copying the contents of the local wp-content folder to the /usr/src/wordpress folder in the container. This is the location of the WordPress source files. These will be copied to the document root by the entrypoint script that is executed right afterward. At the builder stage, we&rsquo;re also setting up a single environment variable to trigger the creation of the wp-config.php file that relies on a set of environment variables to configure database access.
In the docker-compose.yaml file, we reference the custom Dockerfile:
services: app: image: wordpress-local-distroless build: context: . dockerfile: Dockerfile restart: unless-stopped environment: WORDPRESS_DB_HOST: mariadb WORDPRESS_DB_USER: $WORDPRESS_DB_USER WORDPRESS_DB_PASSWORD: $WORDPRESS_DB_PASSWORD WORDPRESS_DB_NAME: $WORDPRESS_DB_NAME WORDPRESS_CONFIG_EXTRA: | # Disable plugin and theme update and installation define( &#39;DISALLOW_FILE_MODS&#39;, true ); # Disable automatic updates define( &#39;AUTOMATIC_UPDATER_DISABLED&#39;, true ); volumes: - document-root:/var/www/html nginx: image: cgr.dev/chainguard/nginx restart: unless-stopped ports: - 8000:8080 volumes: - document-root:/var/www/html - ./nginx.conf:/etc/nginx/nginx.conf mariadb: image: cgr.dev/chainguard/mariadb restart: unless-stopped environment: MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1 MARIADB_USER: $WORDPRESS_DB_USER MARIADB_PASSWORD: $WORDPRESS_DB_PASSWORD MARIADB_DATABASE: $WORDPRESS_DB_NAME ports: - 3306:3306 volumes: document-root:You can now build and run your environment with:
docker compose up --buildThe behavior of this WordPress setup should be similar to the previous examples, but this time, the container image is self-contained and doesn&rsquo;t rely on volumes set up within the host machine to work, in addition to not allowing new package installations or login through a shell. We also set up the WORDPRESS_CONFIG_EXTRA environment variable to disable the installation of new themes and plugins, and to block automatic updates. This increases security by blocking file changes in the container.
To stop the services, type CTRL+C in the terminal where the services are running, and then run:
docker compose downTo keep your WordPress installation up-to-date with latest versions, you can use digestabot, a GitHub Action that works in a similar way to Dependabot, sending a pull request to a repository whenever a new version of a container image is available. This will ensure you&rsquo;re always running the most recent version of WordPress available in Wolfi.
Advanced Usage If your project requires a more specific set of packages that aren't included within the general-purpose WordPress Chainguard Container, 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 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 apk add 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 Container" 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.

