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.

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.CMDorENTRYPOINT: 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.

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_mysqlfor MySQL/MariaDB.gdfor image processing.intlfor Unicode and localization support.zipfor packages and import/export workflows.opcachefor 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:
- Install dev packages.
- Compile PHP extensions.
- Mark runtime libraries as manual.
- Purge dev packages.
- 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.

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
.envand 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.

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
.envfiles. - 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.lockfor 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.