Why teams run Drupal in Docker containers for production
A team that must ship Drupal through a repeatable pipeline into infrastructure it controls needs the same artifact to move from build to test to production without manual edits on each server. Docker answers that need by packaging the Composer-built Drupal codebase into an image, while the database, the uploaded files and the environment-specific settings stay outside it. Containers fit better than hand-managed servers when several environments must match, when rollback should mean redeploying a previous image, and when security releases must be applied on a predictable cycle. Drupal is licensed under GPL-2.0-or-later and there is no paid edition of Drupal itself (Drupal.org licensing), so the cost of this work is engineering effort, not software licensing.
The base image has to follow the PHP requirements of the Drupal version you run. Drupal 11 is the current major version, and Drupal 10 reaches end of life on December 9, 2026. Drupal 10 requires PHP 8.1 or later, Drupal 11 requires PHP 8.3 or later and Drupal 12 requires PHP 8.5 or later, so the PHP minor version pinned in the image is a version decision you make before the build starts. Confirm the exact PHP and extension list against the Drupal PHP requirements page for your target release, and confirm your database client and web server choices against the system requirements before you write the Dockerfile.
The effort in this approach sits in the boundaries you draw. Decide who owns the base image rebuild, how it is tested against your modules, and how quickly it is rebuilt when Drupal publishes a security release on the third Wednesday of the month. Ask your team to confirm which contributed modules need PHP extensions or system packages, because those change the image, and which secrets and file paths the containers must receive at runtime. Those answers drive the image build pipeline, the registry and the release runbook, and they determine how much of the work is one-time setup and how much is ongoing rebuilding.
Container layout options and when each fits
This table compares the main ways to package a Composer-built Drupal codebase into images, from a single all-in-one image to separate web and PHP containers and in-house versus community base images. Read each row for the fit, the main trade-off and what your team must confirm before choosing. The main takeaway is that the web server and database choices decide much of the image design, so settle them first.
| Layout option | Best fit | Main trade-off | What to confirm |
|---|---|---|---|
| Single all-in-one image (PHP-FPM with Nginx or Apache in one container) | Small estates, single sites, teams that want one artifact to build, scan and roll back | Web server, PHP and process supervision share one lifecycle, so scaling and restarts are coarser | Web server choice (Apache or Nginx) and how the image starts both processes |
| Separate web and PHP containers | Teams that scale the web tier and PHP-FPM pools independently, or run on Kubernetes | Two images to version and deploy together, plus a socket or network path to keep correct | Shared file mounts, PHP-FPM pool settings and the path that serves static files |
| Separate web and PHP containers with the web server image from a maintained vendor base | Sites that want fewer patches to the web server layer | Vendor image tags and config layout can change, so the Drupal config must be retested on each update | Which web server image and tag policy the platform team will follow |
| In-house base image built from a minimal OS and pinned PHP | Organizations with strict image provenance rules and their own patch pipeline | Your team owns PHP extensions, OS patches and rebuild cadence for every base update | Who rebuilds the base, how often, and how a security fix reaches all images |
| Community base image (maintained by a third party) | Faster start with standard PHP extensions and sensible defaults | Less control over extensions and build layers, and supply chain review is still required | Image source, maintainer activity, signed tags and whether it supports your PHP version |
| Database outside the image | Most production sites, with MariaDB, MySQL or PostgreSQL run as managed or separate services | Connection settings and credentials must be injected per environment | Database engine and version, and how settings.php reads connection details at runtime |
| SQLite inside a container volume | Demos, tests or low-stakes internal sites only | Data lives in a volume that must be backed up and is hard to scale or share | Whether the Drupal release and contributed modules you need support SQLite |
| Drupal base image with Composer build in a separate stage | Teams that keep build tools out of the runtime image | Multi-stage builds add pipeline steps and need cache discipline | Which PHP version and extensions the runtime image must include |
How to build the Drupal image pipeline step by step
This sequence turns a Composer-managed Drupal codebase into versioned, repeatable container images in which code, files and environment settings each stay in their own place. Each step closes with a check so the pipeline can be trusted before it reaches a production cluster.
Lock the Composer codebase and dependencies
Start in the Git repository. Confirm that composer.json and composer.lock are committed, that core and contributed modules are pinned to exact versions, and that the vendor directory and the sites/default/files directory are excluded from version control. The platform team or the Drupal developer owns this step. Done means a clean checkout runs composer install with the lock file and produces the same tree on two separate machines. Record the Drupal core version you are pinning so the rebuild plan can be checked against the Drupal core release schedule later.
Write a multi-stage Dockerfile
Create a build stage that runs composer install with the --no-dev flag, using a PHP image version that meets the PHP requirement for your Drupal core release. Copy the resulting vendor directory and web root into a slim runtime stage that includes only the PHP extensions and web server your site needs. Keep Composer, Git and build tools out of the runtime stage. Check the result by listing the final image's filesystem and confirming that no dev packages, .git directory or Composer cache are present.
Copy only code into the image
Add a .dockerignore file that excludes sites/*/files, private file paths, local settings files containing secrets, .env files, test fixtures and database dumps. The files directory must never be baked into the image, because it holds user uploads and changes at runtime. Verify by building the image and running a filesystem search inside the container for sites/default/files. The expected result is that the path is either absent or an empty mount point created at runtime, with no uploaded content inside the image layers.
Mount the files directory and private storage
Define the public and private file locations as volumes or object storage paths supplied by the platform, and point Drupal's file system settings at those locations through environment-specific configuration. The operator who runs the cluster or container host owns the storage class or bucket and its backup policy. Verify that a file uploaded in one container is readable from a second container running the same image, and that the directory permissions let the PHP process write to the public files path but not to the code tree.
Read settings from environment-specific includes and secrets
Keep settings.php in the image as a thin loader that includes a file chosen by an environment variable, for example a per-environment settings file for development, staging or production. Inject database credentials, hash salt and other secrets from the orchestrator's secret store as environment variables, not from files in the repository. Set trusted_host_patterns separately for each environment so that each hostname is explicitly allowed. Check by starting the same image in staging and production and confirming that each environment resolves its own database host and accepts only its own hostnames.
Run cron and database updates as separate jobs
Do not run drush updatedb, configuration import and cron inside the web container's startup command. Model each as its own job or deployment step that runs once per release, after the new image is available and before traffic moves to it. Confirm that the web containers start without performing schema changes and that the update job exits successfully and leaves the site in a status with no pending updates. Record the job logs with the image tag so that each release can be traced to its database changes.
Plan image rebuilds from the release schedule and advisories
Set a rebuild routine that follows the Drupal core release schedule, which states that bugfix releases come out on the first Wednesday of each month and security releases on the third Wednesday. Subscribe the security owner to advisories from the Drupal Security Team, because a contributed module or core advisory may require an immediate rebuild outside the routine. Each rebuild should regenerate the lock file from the reviewed update, rerun the image checks above and redeploy through the same job sequence. Done means the team can name the image tag running in production and the advisory or release that triggered its last rebuild.
Separating code, files and settings in a container
The image should be immutable: it carries the Composer-built Drupal codebase and nothing that changes per environment or per visitor. Uploaded content lives in the public and private file paths, which must sit on storage that outlives any container, and the database holds everything else. Drupal's documented settings behavior makes this workable. The settings.php file can load an environment-specific include, such as settings.local.php or a file your build mounts at a known path, and that include can read database credentials, the hash salt and file paths from environment variables supplied by your orchestrator. Confirm with your platform team which volume types, object storage options and secret stores your environment provides, because the answer decides whether public and private files can be shared across replicas and how backups reach them.
Two patterns deliver the runtime settings. Mounting a settings file from a secret or config volume keeps the image identical across environments and makes the file easy to review, but the mounted file still has to be managed and rotated. Generating the settings file at container start from environment variables keeps values out of the filesystem, but the startup script becomes code you must test and version. Either way, never write credentials, the hash salt or a database password into an image layer with COPY, ENV or a build argument. Layers persist in the registry and can be read by anyone with pull access, even if a later layer deletes the file. Scan built images for secrets in your pipeline and confirm who can pull from your registry before the first production release.
Environment-specific settings across development, staging and production
Local development usually runs on a single developer machine or a Compose stack with a throwaway database. Settings here favor speed and visibility over lockdown, so debugging output, verbose logging and relaxed cache settings are normal. The settings file should still load environment-specific values from outside the image, so a developer can point the same image at a local database without editing code.
- Database host: points to a local container service name, with credentials supplied through environment variables or a local, git-ignored override file.
- trusted_host_patterns: lists
localhostand the local ports or hostnames the developer uses, so host header checks still run without blocking local access. - File paths: mounts the public and private file directories to local volumes, which keeps uploads out of the image layers.
- Cache and logging: may disable aggressive page caching and enable verbose logging, but these choices must never be copied into staging or production.
- Configuration split: often uses a development split that enables development modules, which should never be enabled in shared environments.
Before relying on this setup, confirm with your Drupal version which settings files are loaded by default and which override files the project expects.
Staging should behave as closely as practical like production, so it is the environment where configuration, database updates and image changes are proven before release. Its settings should match production in structure and differ only in hostnames, credentials and data sources. Any difference in cache, logging or modules should be documented, because each one can hide a defect that only appears in production.
- Database host: points to a staging database service that is separate from production, with its own credentials and no network path to production data.
- trusted_host_patterns: lists only the staging hostnames, so requests for production hostnames are rejected.
- File paths: uses staging storage, either a separate volume or a separate bucket prefix, so test uploads never mix with production files.
- Configuration split: applies the same production-type split where possible, and any remaining differences should be recorded in the release notes.
- Data handling: if production content is copied for testing, the copy should be scrubbed of personal data according to your own policy, and the process should be confirmed with your privacy and compliance owners.
Check that the staging image is the same digest that will be promoted to production, and that the only differences are the environment values listed here.
Production settings should be the strictest set in the pipeline. Secrets come from a secrets manager or the orchestrator's secret store, mounted at runtime, and never from files committed to the repository or baked into the image. Production also needs settings that protect the site and its data, such as strict host validation and disabled debugging output.
- Database host and credentials: point to the production database service, with credentials injected at runtime and rotated under your secrets policy.
- trusted_host_patterns: lists only the public hostnames for the site, so that host header spoofing is blocked.
- File paths: point to persistent shared storage or object storage, keeping the image stateless.
- Logging and cache backends: send logs to your central collector, and configure the cache backend your architecture uses, such as Redis or Memcache, with settings that match the scaling design.
- Never in the image: production credentials, private keys, API tokens, the
settings.local.phpfile with real values, development modules, and any file that contains live personal data.
Before go-live, confirm the production settings with your Drupal version and verify that no secret appears in the image history or the build logs.
Some settings and files must stay out of the image in every environment, because an image is copied, cached and sometimes shared more widely than any single server. The rule is simple: the image contains code and build output, and everything that identifies an environment or grants access comes from outside at runtime. Reviewing the build context and the final image layers is the practical way to confirm this boundary holds.
- Credentials and keys: database passwords, hash salts, encryption keys, SMTP passwords and API tokens must not be written into
settings.phpor any file copied into the image. - Environment-specific override files:
settings.local.phpand similar files with real hostnames or passwords should be excluded through.dockerignoreand the Git ignore rules. - Uploaded and private files: the public and private file directories are data, not code, so they must be mounted at runtime and never copied at build time.
- Development tooling: development dependencies, debugging modules and test fixtures should not be present in the production image.
- Build-time shortcuts: avoid values that only work on one machine, such as hardcoded host paths.
Ask your team to demonstrate, for each environment, which file supplies each setting, and confirm the exact file names and override order with your Drupal version.
Rebuilding images for security releases without drift
Drupal publishes security releases on the third Wednesday of the month and bugfix releases on the first Wednesday, so image rebuilds should follow that calendar and the advisory dates. Each rebuild must refresh the Composer lock, the base image, the test run and the deployment steps together, or the running containers drift from what was reviewed.
Trigger rebuilds on the release calendar
Set the rebuild pipeline to run on the third Wednesday for security releases and the first Wednesday for bugfix releases, and also when the Drupal Security Team publishes an advisory for a contributed module you use. Subscribe the platform team to those advisories so an out-of-cycle fix does not wait for the next scheduled build. Confirm the release dates against the core schedule each quarter, because a fixed cron rule will not catch an emergency release.
Refresh the Composer lock deliberately
A rebuild that runs composer update without review can pull in unrelated package changes. Regenerate the lock file for the Drupal core and security packages, commit the diff, and compare it against the previous lock before the image is built. Keep composer.json constraints pinned to the supported Drupal minor line so a rebuild cannot silently jump to a new major version.
Rebuild the base image, not just the code
The PHP and web server base image carries its own operating system packages and PHP extensions. Pull a fresh tag from the registry on every rebuild, and pin it by digest in the deployment manifest so the same image is promoted through staging and production. Check the PHP version against the Drupal version in use, since Drupal 11 requires PHP 8.3 or later and Drupal 12 requires PHP 8.5 or later.
Rerun the full test suite
Run functional tests, the Drupal update path checks and a smoke test against a copy of the production database before the image is promoted. Include the contributed modules in the run, because a core security fix can break a module that has not yet released a compatible version. If a contributed module lags, decide whether to hold the release, patch it, or accept a documented exception with a date to revisit.
Run database updates and configuration import
A new image does not change the database. The deployment job must run database updates and configuration import in a defined order before traffic reaches the new containers, and it must fail loudly if either step errors. Keep the database backup taken immediately before the deploy so a failed update can be reverted to the previous image and database state together.
Detect drift between environments
Drift appears when staging runs a digest that production does not, or when a container was patched by hand. Record the image digest, Composer lock hash and core version in each environment, and alert when they differ from the promoted release. Ask your team to confirm who approves an out-of-band patch and how it is merged back into the repository, so the next rebuild does not reverse it.
Questions teams ask before running Drupal in Docker
What drives the cost of containerizing a Drupal site?
The cost depends mostly on how far the current codebase is from a clean, Composer-built state and how many environments need to be reproduced. Teams pay for discovery of the existing site, a Dockerfile and image build pipeline, separation of code from files and settings, and handover documentation. The main cost multipliers are custom or contributed modules that rely on writable paths, hand-edited core files, multisite layouts, and the number of environments and registries in scope. Ongoing costs are the people who rebuild images, rotate base images and run releases. Ask any provider for a written scope that lists each environment, each deliverable and who maintains the pipeline after handover. No figure is given here because cost depends on the codebase and the environment.
How long does a first Drupal container pipeline usually take?
Duration is set by the state of the codebase, the number of environments and the rules your organization applies to build, registry and deployment. The work usually falls into a few phases: auditing the repository and Composer lock file, writing the image and build configuration, moving files and secrets out of the repository, wiring the pipeline to your registry and running it in staging. Teams with clean Composer projects and an existing registry move faster. Teams that need change approval, air-gapped package mirrors or a signed-image policy add time. Ask for milestones with named exit criteria in the written scope rather than a single end date.
What must our team provide before a containerization project starts?
Most of the dependency sits with the client. A typical list includes:
- Read access to the Drupal repository, including the composer.json and composer.lock files and the custom code
- Access to staging and production environments, or a copy of their configuration, so the container matches what runs today
- A decision on where persistent files live, such as a shared volume or object storage, and access to that storage
- The secrets management tool the organization already uses, with a named owner for database credentials and the hash salt
- A container registry, a deployment target and named release owners who approve changes and security rebuilds
Confirm these items in the scope before work begins, because missing access is one of the most common causes of delay.
Can an in-house team containerize Drupal without outside help?
Yes, a team with Drupal, Composer and container experience can do this internally, and many do. The questions to answer honestly are whether anyone has built a production image before, whether the team can test the upgrade path for each Drupal release, and whether there is capacity to own the pipeline after launch. Drupal needs a settings file per environment, file storage outside the image and a deploy step that runs database updates and configuration import. If those are new skills, an outside partner can plan and build the first version, document it and hand over. DeployOpen implements, documents and supports self-hosted platforms in your infrastructure, so the choice between your team and a partner should rest on skills, capacity and the written scope.
How do image rebuilds fit the Drupal security release calendar?
Security releases for Drupal core and contributed projects follow a fixed rhythm. According to the Drupal core release schedule, bugfix releases come out on the first Wednesday of each month and security releases on the third Wednesday. Your pipeline should therefore be able to rebuild an image from the same lock file with an updated Drupal or module version within a short window after each security release, and it should be tested in staging before production. Plan who approves emergency rebuilds, how base PHP and web server images are refreshed, and how old images are retired. Confirm the process in your written scope and with your release owners.
Which Drupal and PHP versions should a new container image target?
Pick the Drupal major version first, then the PHP version it requires. According to the Drupal.org PHP requirements page, Drupal 10 requires PHP 8.1 or later, Drupal 11 requires PHP 8.3 or later and Drupal 12 requires PHP 8.5 or later. The Drupal core release schedule lists Drupal 11 as the current major version and states that Drupal 10 reaches end of life on December 9, 2026. A new image should therefore not target a version that will reach end of life soon after launch. Sites still on Drupal 7 cannot be containerized as a simple update, because security support ended on January 5, 2025, and they must be migrated first. Ask your team to confirm the target version in writing.
Does the container approach affect licensing or vendor lock-in?
Drupal and the contributed files hosted on Drupal.org that are derivative works of Drupal are licensed under GPL-2.0-or-later, according to the Drupal.org licensing page, and there is no paid edition of Drupal itself. Containerizing Drupal does not change that license. Your own custom modules and themes carry the license you choose, and some third-party or commercial modules and base images have their own terms. Review the license of every image layer and every module you add, and confirm with your legal or procurement team which terms apply to your build.
How to verify the container setup before go-live
Verification comes down to proving that the image behaves the same way in every environment and that nothing writes outside the paths you planned. Start by running the built image on its own, outside the cluster, and confirm that Drupal boots with the PHP version the image declares. Drupal 10 requires PHP 8.1 or later, Drupal 11 requires PHP 8.3 or later, and Drupal 12 requires PHP 8.5 or later, according to the Drupal.org PHP requirements page. Record the PHP and extension versions from the running container, not from the Dockerfile, so the evidence matches what actually ships.
Next, open the status report at /admin/reports/status in a container started with production-like settings and confirm there are no environment errors, such as unwritable directories, missing PHP extensions or a database driver mismatch. Then test the file boundary. Upload a file, generate image styles and trigger a cron run, and confirm that new writes land only in the mounted public and private file paths. Inspect the container's writable layer for stray files under the web root. Finally, send requests with an unknown Host header and confirm that trusted_host_patterns rejects them, and that the expected hostname for each environment is accepted.
Repeat the same tests after every security rebuild on a new base image, since a changed base can alter PHP, OpenSSL or library versions without any change to your code. Keep the test script in the repository so the same checks run in CI and before each promotion. Ask DeployOpen, or your own team, to deliver the test scripts, the recorded outputs for each image digest, a list of the checks that failed and how each was resolved, and a short runbook describing who repeats the checks after a Drupal security release. These records are evidence for your own review. They show what was tested and when. They are not an audit report, and an independent auditor will decide what they mean for your compliance program.
How DeployOpen scopes and delivers Drupal container work
DeployOpen plans, builds, documents and supports self-hosted open-source platforms inside your own infrastructure, including Drupal container images that run in your cloud accounts, data centers or air-gapped networks. Engagements are fixed-scope, with scope, deliverables and timelines agreed in writing before work starts.
How we scope
- Written scope lists the Drupal version, PHP version and database engine your containers will run.
- Scope records which image registry, build pipeline and runtime platform your team already operates.
- Scope defines how settings, secrets, private files and environment configuration are separated from the image.
How we build
- Composer-built codebase is packaged into images with a pinned base image and locked dependencies.
- Environment-specific settings.php includes are wired so one image runs in development, staging and production.
- Image rebuild process is tested so a Drupal security release can be applied through the same pipeline.
How we hand over
- Handover covers Dockerfiles, build scripts, environment variable reference and runbooks for your team.
- Deliverables include a documented procedure for rebuilding and redeploying images after a security advisory.
- Your engineers run a rehearsal of build, deploy and rollback before the engagement closes.
How we support
- Ongoing support, upgrades and continuous testing are available as a retainer after handover.
- Retainer work can cover base image updates, Drupal release upgrades and review of security advisories.
- Ask for the support boundaries, response arrangements and change process in the written quote.

