Dockerfile Best Practices for Drupal Applications

Drupal

Dockerfile Best Practices for Drupal Applications

A practical guide to writing Dockerfiles for Drupal: multi-stage builds, Composer dependencies, PHP extensions, WebP support, Apache docroot, .dockerignore, file permissions, secrets, and CI image verification.

A Dockerfile is the recipe for your application image. For Drupal, that image usually contains PHP, the web server, Composer dependencies, Drupal core, contrib modules, custom code, and the PHP extensions your site needs. The database, uploaded files, secrets, and environment-specific settings should stay outside the image.

A good Dockerfile gives you a repeatable release artifact. A bad Dockerfile creates slow builds, huge images, missing PHP extensions, permission problems, leaked secrets, and “works on my machine” production surprises.

Drupal Dockerfile build flow from base image to runtime container
A Dockerfile should separate build-time work from runtime behavior.

What A Dockerfile Does

A Dockerfile is a list of instructions used by Docker Build to create an image. Common instructions include:

  • FROM: choose a base image.
  • WORKDIR: set the working directory.
  • COPY: copy files from the build context into the image.
  • RUN: execute build-time commands.
  • ENV: set environment variables.
  • CMD or ENTRYPOINT: define what runs when the container starts.

For Drupal, the Dockerfile must answer practical questions:

  • Which PHP version runs the site?
  • Which PHP extensions are installed?
  • Where is the web server document root?
  • How are Composer dependencies installed?
  • What files are copied into the image?
  • Which directories are writable at runtime?
  • How do secrets and environment-specific settings reach Drupal?

Use Multi-Stage Builds

Docker multi-stage builds let you use one stage to build the application and another stage to run it. Docker's official documentation recommends multi-stage builds to separate build tools from runtime images.

Multi-stage Dockerfile for Drupal with vendor stage and runtime stage
Build dependencies in one stage, then copy the finished application into the runtime image.

A simplified Drupal pattern:

FROM php:8.3-cli-bookworm AS vendor

WORKDIR /app
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

COPY composer.json composer.lock ./
RUN composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader

COPY . .
RUN composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader

FROM php:8.3-apache-bookworm

WORKDIR /var/www/html
COPY --from=vendor --chown=www-data:www-data /app /var/www/html

The vendor stage can have Composer, Git, unzip, and build libraries. The final Apache stage should be smaller and focused on running Drupal.

Install PHP Extensions Deliberately

Drupal commonly needs extensions such as:

  • pdo_mysql for MySQL/MariaDB.
  • gd for image processing.
  • intl for Unicode and localization support.
  • zip for packages and import/export workflows.
  • opcache for production PHP performance.

For image handling, be explicit about GD features. If Drupal image styles should create WebP derivatives, GD must be compiled with WebP support:

RUN apt-get update \
  && apt-get install -y --no-install-recommends \
    libfreetype6-dev \
    libjpeg62-turbo-dev \
    libpng-dev \
    libwebp-dev \
  && docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
  && docker-php-ext-install -j"$(nproc)" gd

After deployment, verify inside the container:

php -r 'var_export(gd_info());'

You should see 'WebP Support' => true when WebP image styles are expected.

Keep Runtime Images Clean

Development libraries are needed to compile PHP extensions. They are not always needed at runtime. A common pattern is:

  1. Install dev packages.
  2. Compile PHP extensions.
  3. Mark runtime libraries as manual.
  4. Purge dev packages.
  5. Clean package lists.

Example:

RUN apt-get update \
  && apt-get install -y --no-install-recommends libwebp-dev libzip-dev \
  && docker-php-ext-install zip \
  && apt-mark manual libwebp7 libzip4 \
  && apt-get purge -y --auto-remove libwebp-dev libzip-dev \
  && rm -rf /var/lib/apt/lists/*

This reduces image size and attack surface. It also makes image scanning less noisy.

Use .dockerignore

The build context is everything Docker can see when building the image. If .dockerignore is weak, Docker may send unnecessary or dangerous files into the build.

Dockerfile security checklist for Drupal images
Many container mistakes begin with the build context. Keep it clean.

For Drupal, exclude:

  • .git/
  • .ddev/
  • vendor/
  • web/core/
  • web/modules/contrib/
  • web/themes/contrib/
  • web/sites/*/files/*
  • database dumps
  • logs
  • backups
  • node_modules/
  • private files
  • .env and local secrets

Composer should rebuild vendor dependencies inside the image from composer.lock. Uploaded files should come from a mounted volume or object storage, not from the image.

Configure The Drupal Web Root

Modern Composer-based Drupal projects keep the public document root in web/. Apache should serve /var/www/html/web, not the repository root.

ENV APACHE_DOCUMENT_ROOT=/var/www/html/web

RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
  && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}/!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf

This prevents files above the web root, such as Composer metadata and configuration scaffolding, from being served by the web server.

Handle Files Directory Correctly

Drupal needs web/sites/default/files to be writable. But that does not mean uploaded files belong inside the image.

Drupal runtime concerns in Dockerfile including Apache docroot files directory PHP extensions and config
Runtime state should be mounted or provided by the environment, not baked into the image.

In the image, create the directory and set ownership:

RUN mkdir -p web/sites/default/files \
  && chown -R www-data:www-data web/sites/default/files

In production, mount persistent storage at that path, or use a hosting pattern that syncs files from durable storage. If the container is destroyed, the image should be replaceable without losing uploaded files.

Do Not Bake Secrets Into Images

Docker's official guidance is to use build secrets when a build needs sensitive data, and runtime secrets or environment variables when the running application needs sensitive data. Do not copy secrets into the image.

Never bake these into a Docker image:

  • Database credentials.
  • Drupal hash salt.
  • API keys.
  • Private keys.
  • GitHub tokens.
  • Production .env files.
  • Database dumps.
  • Private uploaded files.

For Drupal, use environment-specific settings.php logic, mounted secret files, container orchestration secrets, or environment variables depending on your hosting stack.

Think About Build Cache

Docker builds are faster when stable layers happen before frequently changing layers. That is why Composer files are copied before the rest of the source:

COPY composer.json composer.lock ./
RUN composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader

COPY . .
RUN composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader

If only a custom module changes, Docker can reuse the expensive Composer dependency layer. If composer.lock changes, Docker correctly rebuilds vendor dependencies.

Build And Verify Locally

Basic commands:

docker build -t technical-blog:local .
docker run --rm technical-blog:local php -v
docker run --rm technical-blog:local php -m
docker run --rm technical-blog:local php -r 'var_export(gd_info());'

For Apache images, run the container and check the site:

docker run --rm -p 8080:80 technical-blog:local
curl -I http://127.0.0.1:8080/

In CI, build pull requests but push images only from trusted branches or tags. Use image tags that tell you what is running: branch, pull request, SHA, and release tag.

Common Mistakes

Copying The Whole Local Project Into The Image

If .dockerignore is weak, you may copy local files, vendor directories, dumps, or secrets into the image. Fix the build context first.

Installing Composer Dependencies After Copying Everything

This makes every source-code change invalidate the Composer layer. Copy composer.json and composer.lock first.

Leaving Build Tools In The Runtime Image

Git, compilers, and dev libraries often belong in the build stage, not the final runtime stage.

Forgetting PHP Extension Features

Installing gd is not enough if you need JPEG, PNG, FreeType, or WebP support. Compile and verify the exact capabilities.

Baking Uploaded Files Into The Image

Images are replaceable artifacts. Uploaded files are runtime state. Keep them separate.

Production Checklist

  • Use a multi-stage build.
  • Pin a supported PHP base image and review it regularly.
  • Install only required PHP extensions.
  • Compile GD with the image formats Drupal needs.
  • Use composer.lock for repeatable dependency installs.
  • Exclude files, dumps, logs, secrets, and local tooling with .dockerignore.
  • Set Apache or Nginx docroot to Drupal's web/ directory.
  • Keep uploaded files in persistent storage, not in the image.
  • Inject secrets at runtime.
  • Scan images in CI.
  • Tag images with branch, SHA, and release identifiers.

Reference Links

Final Takeaway

A Drupal Dockerfile should produce a boring, repeatable runtime image. It should build Composer dependencies, install PHP extensions, configure the web server, copy the application, and leave runtime state to volumes and environment configuration.

When the Dockerfile is clean, deployment becomes simpler: build once, scan once, tag once, pull the image on the server, run Drupal deploy commands, and know exactly what code is live.

Keep reading

Drupal Sep 7, 2026 6 min read

Build a Drupal Chatbot with Local AI

Learn how a Drupal chatbot finds relevant published articles, uses Ollama to generate answers, and displays source links while keeping inference on your own hardware.