# Getting Started with the WordPress Chainguard Container

URL: https://deploy-preview-3420--ornate-narwhal-088216.netlify.app/chainguard/chainguard-images/getting-started/wordpress.md
Last Modified: June 9, 2026
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 is a drop-in replacement for the official WordPress FPM-Alpine image, with significantly fewer vulnerabilities than the standard image. It includes a distroless variant for production use that removes shells, package managers, and other unnecessary components. The image ships with the latest PHP and WordPress versions and all required PHP extensions.
This guide covers three ways to 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 guide requires Docker. Download and install it from the official Docker website if you don&rsquo;t have it.
The images in this guide require a free Chainguard account. Sign up at chainguard.dev if you don&rsquo;t have one, then authenticate:
chainctl auth loginIf you encounter credential errors when pulling images, pull them individually before running docker compose:
docker pull cgr.dev/chainguard/wordpress:latest-dev docker pull cgr.dev/chainguard/wordpress:latest docker pull cgr.dev/chainguard/mariadb docker pull cgr.dev/chainguard/nginx Clone the demos repository Clone the demos repository to your local machine:
git clone git@github.com:chainguard-dev/edu-images-demos.gitNavigate to the wordpress demo directory:
cd edu-images-demos/php/wordpressYou&rsquo;ll find three directories, one for each example in this guide.
Example 1: Testing the container image with a fresh WordPress install The latest-dev variant of the Chainguard WordPress Container lets you run a fresh WordPress installation and explore the setup wizard. Changes you make won&rsquo;t persist after you stop the environment — the next example shows how to persist customizations using a volume.
The files for this example are in the 01-preview directory. Open docker-compose.yml in your editor to follow along.
This example includes a Dockerfile that copies WordPress source files into the document root and sets ownership at build time:
FROM cgr.dev/chainguard/wordpress:latest-dev USER root RUN cp -r /usr/src/wordpress/. /var/www/html/ &amp;&amp; \ cp /var/www/html/wp-config-docker.php /var/www/html/wp-config.php &amp;&amp; \ chown -R php:php /var/www/html USER phpThe docker-compose.yml for this example:
services: app: build: . image: wordpress-preview 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:This Docker Compose file defines three services: app, nginx, and mariadb:
The app service builds a local image using the included Dockerfile, which copies WordPress source files from /usr/src/wordpress into the document root and renames wp-config-docker.php to wp-config.php. That config file reads database credentials from environment variables at runtime. The document-root volume is shared between app and nginx. The nginx service uses the Chainguard nginx Container, configured to serve the WordPress application on port 8000. The mariadb service uses the Chainguard MariaDB Container, configured with the environment variables needed to create the WordPress database. The environment variables are stored in a .env file in the same directory. To view them, run:
cat .envWORDPRESS_DB_HOST=mariadb WORDPRESS_DB_USER=wp-user WORDPRESS_DB_PASSWORD=wp-password WORDPRESS_DB_NAME=wordpressYou can change these values as needed. The .env file is hidden and won&rsquo;t appear in most file explorers, but you can open it in any terminal text editor.
Build the image and start the services:
docker compose up --buildOpen http://localhost:8000 in your browser to reach the WordPress installation page. Follow the on-screen instructions to complete setup. Any customizations will be lost when you stop the environment.
To stop the services, press CTRL+C in the terminal, then run:
docker compose downThis removes the containers and networks. The next example shows how to mount a volume so that customizations like themes and plugins persist.
Example 2: Customizing a new WordPress installation To keep customizations — themes, plugins, and other changes — between container rebuilds, you need a volume with the correct permissions. This requires a user inside the container with the same UID as your host user. This example uses a custom Dockerfile to add a wordpress user with a configurable UID (defaulting to 1000, the typical UID for a non-root user on Linux) and sets ownership of the document root accordingly.
Navigate to 02-customizing to follow along. Here&rsquo;s the Dockerfile:
FROM cgr.dev/chainguard/wordpress:latest-dev ARG UID=1000 USER root RUN cp -r /usr/src/wordpress/. /var/www/html/ &amp;&amp; \ cp /var/www/html/wp-config-docker.php /var/www/html/wp-config.php &amp;&amp; \ cp -r /usr/src/wordpress/wp-content /usr/src/wp-content-default RUN addgroup wordpress &amp;&amp; adduser -SD -u &#34;$UID&#34; -s /bin/bash wordpress wordpress RUN chown -R wordpress:wordpress /var/www/html /usr/src/wp-content-default COPY docker-entrypoint.sh /docker-entrypoint.sh RUN chmod &#43;x /docker-entrypoint.sh USER wordpress ENTRYPOINT [&#34;/docker-entrypoint.sh&#34;]In addition to creating the wordpress user, the Dockerfile copies WordPress source files into the document root at build time and saves a copy of the default wp-content directory to /usr/src/wp-content-default. A custom entrypoint script uses that copy to populate the host-mounted wp-content directory on first run if it doesn&rsquo;t yet contain a themes subdirectory:
#!/bin/sh # Populate wp-content from defaults on first run (when themes directory is absent) if [ ! -d /var/www/html/wp-content/themes ]; then cp -r /usr/src/wp-content-default/. /var/www/html/wp-content/ fi exec php-fpmThe docker-compose.yml file references the custom Dockerfile and accepts a UID 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 differs from the previous example. The build section references the custom Dockerfile and sets UID to 1000 by default; pass your own UID at build time to match your host user. The bind mount at ./wp-content persists the WordPress content directory to the host.
Build the image, passing your UID as a build argument:
docker compose build --build-arg UID=$(id -u) appOnce the build completes, start the services:
docker compose upOpen http://localhost:8000 in your browser to access the WordPress installation.
In a separate terminal, check that the wp-content directory 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 themesThe matching UID between the container&rsquo;s wordpress user and your host user is what allows files written by the container to be owned by your host account.
Any themes and plugins you install now persist between container rebuilds.
To stop the services, press CTRL+C in the terminal, then run:
docker compose downThe next example shows how to build a distroless WordPress image for production.
Example 3: Using the distroless variant of the WordPress container image This example uses a multi-stage Docker build to produce a distroless image with a smaller attack surface. The distroless image includes only the dependencies needed to run WordPress, without a shell or package manager.
The key difference from the previous examples is that all WordPress files — including any customizations in wp-content — are baked into the image at build time rather than populated at runtime. This makes the image self-contained: it doesn&rsquo;t rely on init steps or host volumes. Adding custom content at build time increases the final image size, but prevents filesystem changes once the container is running.
To demonstrate custom content, this example includes the Cue blogging theme and the Imsanity image-resizing plugin.
Navigate to 03-distroless to follow along. Here&rsquo;s the Dockerfile:
FROM cgr.dev/chainguard/wordpress:latest-dev AS builder #copy wp-content folder COPY ./wp-content /usr/src/wordpress/wp-content USER root #copy WordPress source to document root and set up config RUN cp -r /usr/src/wordpress/. /var/www/html/ &amp;&amp; \ cp /var/www/html/wp-config-docker.php /var/www/html/wp-config.php FROM cgr.dev/chainguard/wordpress:latest COPY --from=builder --chown=php:php /var/www/html /var/www/htmlThe COPY instruction places your local wp-content into /usr/src/wordpress/wp-content before the RUN step merges it with the rest of the WordPress source. USER root is required because the WordPress source directory in this image is only readable by root. The RUN command copies everything to /var/www/html and renames wp-config-docker.php to wp-config.php, which reads database credentials from environment variables at runtime. The final stage copies the populated document root into the distroless image with php:php ownership.
The docker-compose.yml file references 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:Build and start the environment:
docker compose up --buildThis WordPress setup behaves like the previous examples, but the image is self-contained — it requires no host volumes and allows no new package installations or shell access. The WORDPRESS_CONFIG_EXTRA environment variable disables theme and plugin installation and automatic updates, preventing filesystem changes inside the running container.
To stop the services, press CTRL+C in the terminal, then run:
docker compose downTo keep your WordPress installation up to date, use digestabot, a GitHub Action that works like Dependabot — it sends a pull request to your repository whenever a new version of a container image is available, ensuring you&rsquo;re always running the latest WordPress version from 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.

