Docker
The Docker overview describes the tool as follows:
Docker is an open platform for developing, shipping, and running applications.
Vortex ships a Docker and Docker Compose configuration that runs the
project in containers. A single docker-compose.yml describes the stack
locally, in CI and on container-based hosting such as Lagoon. Comment
markers flag its few local-only lines (###) and CI-only lines (##).
This page is for developers who change the stack or need to know how it fits together. It covers the following:
- How to reach the local site and its database from your machine
- What each service in
docker-compose.ymldoes - How code and environment variables reach the containers
- What the image build leaves out
- How to check
docker-compose.ymlafter you edit it
Special thanks to Lagoon for the container images the stack builds on. The Lagoon team maintains these images, which run in production environments and get regular updates. If you host on Lagoon, your local, CI and production environments all build the project from the same base images.
About Docker and Docker Compose
Docker runs each service your project needs, such as a web server or a database server, in its own container. A container is an isolated environment that can still talk to the other containers.
Containers start from images, templates that define what's installed in a container and how it's configured, so every container started from an image has the same content and configuration.
Docker is the engine that runs those containers: it lets them share the
host's resources and talk to each other. Locally, you control it with the
Docker CLI, docker.
Docker Compose describes a
multi-container application in a single docker-compose.yml file, so all
the containers that work together are defined and managed in 1 place.
Drupal needs several service containers, so you mostly use the Docker
Compose CLI, docker compose. Its commands work on the docker-compose.yml
in the current directory, so run them from the project root. They affect
only the containers that file defines.
After a project is set up, day-to-day work comes down to a handful of commands, each scoped to that project:
docker compose up -d- start the containers in the background.docker compose down- stop and remove the containers.docker compose exec <service> <command>- run a command in the running<service>container.docker compose logs -f- follow the logs of all the containers.docker compose ps- list the running containers.
You need the more advanced commands mainly when you change the project's Docker configuration or services. For anything beyond this overview, see the Docker documentation and the Docker Compose documentation.
Reach the local stack
Vortex runs the project in containers locally and in CI. Ahoy wraps the common commands, and you can copy any of them into your terminal to run it without Ahoy. The Environment section covers the commands for each task.
Local site address
The local site runs at http://<project>.docker.amazee.io, where
<project> is the Docker Compose project name. The project name defaults
to the name of the project directory. ahoy info prints the full URL as
Site local URL.
To change the project name, and with it the URL, follow these steps:
- Run
ahoy downto remove the current containers and their database. Docker Compose groups containers by project name, so after the change, Ahoy commands no longer find the old containers. - Set
COMPOSE_PROJECT_NAMEin.env. - Run
ahoy upto start a new set of containers under the new name. - Run
ahoy provisionto set up the site in the new containers.
Database connection
To connect a database client on your machine to the database service,
use these details:
- Host:
127.0.0.1 - Port: the number that
ahoy infoprints asDB port on host - Database, username and password:
drupal
ahoy info also prints DB host and DB port, but those values work only
inside the containers. Docker assigns the host port when the container
starts, so the port can change after a restart. If the client can't
connect, run ahoy info again.
docker-compose.yml anatomy
Docker Compose reads its configuration from docker-compose.yml, and when
docker-compose.override.yml exists, Compose merges it on top. Git ignores
the override file, so it's the place for changes that apply only to your
machine. Both are YAML files, and YAML supports anchors and
references that cut down on
repetition.
The file that ships with Vortex has these sections:
Volumes definitions
Volumes share data between containers. When the host supports volume mounting, your machine shares the data too: you edit a file in your editor, and the running containers get the change straight away.
Local development relies on this: the x-volumes anchor mounts the code
into the cli, nginx, php and chrome containers. It defines 2
mounts:
- Project root directory
.maps to/appwithin a container. PHP reads the application code from there. - Files directory
./web/sites/default/filesmaps to/app/web/sites/default/fileswithin a container, overriding the first mount for that path. The separate mount lets this directory use a different type of syncing for performance, because files change less often than code.
The anchor also carries 2 commented-out mounts for environments without
volume mounting support, such as CircleCI, whose containers run on a
remote Docker server. Before CI starts the stack, on GitHub Actions and
CircleCI alike, it edits docker-compose.yml to delete every line marked
###. That drops the host mounts. CI also uncomments every line marked
##, which swaps in the named volumes app and files that the
top-level volumes section declares.
The top-level volumes section also declares solr for the Solr index
when Solr is enabled. When you host on Lagoon with Storybook enabled, it
declares storybook, a Lagoon persistent volume.
The VOLUME_FLAGS environment variable sets the consistency mode of the
host mounts. It takes these values:
default: Equivalent toconsistent.consistent: Full consistency. The container runtime and the host maintain an identical view of the mount at all times.cached: The host's view of the mount is authoritative. Updates made on the host might take a moment to appear in a container.delegated: The container runtime's view of the mount is authoritative. Updates made in a container might take a moment to appear on the host.
When VOLUME_FLAGS is unset or empty, the mounts use delegated, not
default. delegated favors the container's write performance during
heavy operations, such as installing dependencies. The cost is that
changes made in a container take a moment to appear on the host.
To use another mode, set VOLUME_FLAGS in .env and run ahoy up, which
recreates the containers with the new mounts.
Default user
The x-user anchor sets user: '1000', the numeric user ID that container
processes run as. The nginx, php and database services use it, and so
does the optional database2. On Linux, files a container writes into a
host mount keep that numeric ID. With matching IDs, the files the site
writes belong to you, such as uploads in web/sites/default/files.
On Linux, if your user ID isn't 1000 (id -u prints it), change the
value in docker-compose.yml, then run ahoy up to recreate the
containers that use it. The file is committed, so the change applies to
everyone on the project. To change it only on your machine, set user on
the services that use the anchor in docker-compose.override.yml instead.
The cli service runs as root instead of using the anchor. On Linux,
files it creates in the project can end up owned by root on your
machine, such as a freshly installed vendor directory. For the fix, see
Permission issues.
Container environment variables
Docker Compose reads the .env file in the project root and uses the
values to fill in the ${VAR} references in docker-compose.yml. A
variable already set in your shell takes precedence over the same variable
in .env. Compose doesn't pass .env to the containers wholesale: it sets
only the variables a service lists under environment.
In this file, the services list them through the x-environment anchor,
which every service except redis and wait-for-dependencies merges in.
Compose sets the values when it creates a container, so a change reaches a
running container only after ahoy up or docker compose up -d recreates
it.
The cli, nginx and php services build on Lagoon images, which load
.env themselves. When a container starts, the image's entrypoint reads
.env from /app before it launches the main process. A variable the
container already has wins over its .env value. In the php container,
the main process is PHP-FPM, which passes the loaded variables on to every
request. That's how Drupal gets the .env variables that
docker-compose.yml doesn't list, such as DRUPAL_CLAMAV_ENABLED.
PHP-FPM keeps the values it started with. A change to such a variable
doesn't alter the container's configuration, so ahoy up leaves the
container running. Run ahoy restart or docker compose restart instead,
so the entrypoint reads .env again.
Docker Compose and the Lagoon images never read .env.local. Vortex
loads it in 2 places, and both keep any variable that's already set in your
environment:
- The Ahoy entrypoint exports
.envand then.env.localbefore every Ahoy command, soahoy upfills the${VAR}references with your local overrides. A baredocker compose up -ddoesn't get them. - Every
vendor/bin/vortex-*script loads.envand then.env.localfrom its working directory when it starts, on the host and inside the containers.
For a variable that docker-compose.yml doesn't list, web requests get the
value from .env, never from .env.local. To use a value from
.env.local, list the variable in docker-compose.override.yml, as
Environment variables shows.
Inside the cli container, commands get variables from 2 more sources:
ahoy cliand every Ahoy command built on it forward your shell'sTERMandLOCALDEV_URL, plus any variable that starts withCOMPOSE_,GITHUB_,PACKAGE_,DOCKER_,DRUPAL_,VORTEX_orENVIRONMENT_. The values reach the command on its next run, with no container recreated.- The
~/.bashrcin the Lagoon images reads.envat the start of every bash session, so commands run throughbashget the.envvariables thatdocker-compose.ymldoesn't list.~/.bashrckeeps any variable the container already has. A process Docker starts directly, such asdocker compose exec cli php, skips~/.bashrc.
The x-environment anchor itself holds only 2 kinds of variables:
- Variables that are specific to the stack and need a default value, such
as
LAGOON_ROUTE. - Variables that can't live in
.envand come from the actual environment instead, such as secrets.
Any other variable belongs in .env. The anchor defines these variables:
TZ, which defaults toUTC- The pass-through variables
CI,ENVIRONMENT_TYPEandXDEBUG_ENABLE LOCALDEV_URLandLAGOON_ROUTE, both set to the local development URLDATABASE_HOST,DATABASE_NAME,DATABASE_USERNAME,DATABASE_PASSWORD,DATABASE_PORT,DATABASE_CHARSETandDATABASE_COLLATION, fixed to the values of the localdatabaseserviceDRUPAL_TRUSTED_HOSTS,DRUPAL_THEME,DRUPAL_PUBLIC_FILES,DRUPAL_PRIVATE_FILES,DRUPAL_TEMPORARY_FILESandDRUPAL_CONFIG_PATH- With Shield,
DRUPAL_SHIELD_USERandDRUPAL_SHIELD_PASS - With Redis,
DRUPAL_REDIS_ENABLED - With the migration database,
DATABASE2_HOST,DATABASE2_NAME,DATABASE2_USERNAME,DATABASE2_PASSWORDandDATABASE2_PORT
The nginx service adds 1 pass-through variable of its own,
VORTEX_WEBSERVER_MAINTENANCE_MODE, which turns on the static maintenance
page.
In CI, the ## marker also switches on SSMTP_MAILHUB, which points
outgoing mail at a closed port, so no message leaves a test run. If you
host on Lagoon, its environments don't read the x-environment values
from docker-compose.yml.
The following 3 lines from the anchor show how a value gets its default:
TZ: ${TZ:-UTC}
# Local development URL.
LOCALDEV_URL: &default-url ${COMPOSE_PROJECT_NAME:-example-site}.docker.amazee.io
# Local development route used in Lagoon images and Pygmy to route requests.
LAGOON_ROUTE: *default-url
Each of these lines does the following:
TZ: ${TZ:-UTC}- takesTZfrom your environment or.envand falls back toUTCwhenTZis unset or empty.LOCALDEV_URL: &default-url ${COMPOSE_PROJECT_NAME:-example-site}.docker.amazee.io- setsLOCALDEV_URLto<project>.docker.amazee.io, where<project>is the Docker Compose project name. The project name defaults to the name of the project directory, andexample-siteapplies only when it's empty. UnlikeTZ, this line has no fallback to an existingLOCALDEV_URL. The YAML anchordefault-urlstores the value for later use.LAGOON_ROUTE: *default-url- setsLAGOON_ROUTEto the same value through the*default-urlalias, so the 2 always match.
For how Compose handles environment variables in detail, see Set environment variables within your container's environment in the Docker documentation.
Services
The services section configures each container. The docker-compose.yml
that Vortex ships defines the following services. The optional ones are
there only when you enable the matching feature in the installer:
-
cli- runs a shell for commands in the context of the project, such ascomposerordrush, and never handles web requests. Thenginxandphpbuilds copy the application code from thecliimage, so all 3 contain the same code. On Lagoon, cron jobs run in this container too.The build gets
PACKAGE_TOKEN, which you set in.env.local, as thepackage_tokenbuild secret, so Composer can reach private repositories. Compose also passes the token as a build argument on every build. The Composer step uses that argument when the secret is empty. Docker records build argument values in the image history, so the token can end up in the image. -
nginx- runs the web server, which serves the application and passes requests on to thephpcontainer. It's the only service on Pygmy'samazeeio-network. -
php- runs PHP-FPM, which executes the application code for the requests thatnginxpasses on. Thephpimage lacks the command-line tools that thecliimage adds, such as Composer, Node.js and the database client, so it's smaller. In production,phpis the container that scales up and down. -
database- runs the database server that stores the application data. You can reach it from your host on a random port, whichahoy infoshows.docker compose port database 3306prints it too. SetVORTEX_DB_IMAGEto run a pre-built image with the database already inside. -
database2- an optional second database container holding the source database for migrations. It's present only when the migration feature is enabled. -
redis- an optional container that runs a Redis-compatible (Valkey) server, which stores the application cache. -
solr- an optional container that runs an Apache Solr 9 search server, which stores the application's search index and provides full-text search.The image is built from
.docker/solr.dockerfileon top of the Lagoonuselagoon/solr-9-drupalimage. The configuration set lives in.docker/config/solr/config-set/, so you can customize the Solr schema and keep it under version control. You reach Solr from the host on a random port, whichdocker compose port solr 8983prints. The Solr admin interface is athttp://localhost:<port>/solr/. -
clamav- an optional container that runs a ClamAV antivirus server, which Drupal uses to scan uploaded files for viruses. -
chrome- runs Chromium, the open source browser that Chrome is built from, for the Behat tests. It's based on the official Selenium image, which comes with the extra tools a browser needs in a container, such as a virtual desktop.ahoy infoprints a VNC URL where you can watch that desktop while the tests run. -
wait-for-dependencies- runs a script that waits for the applications in other containers to accept connections. Docker Compose starts containers in dependency order, but a started container isn't always ready. This container polls thedatabaseport and, when they're present, theclamav,database2,redisandsolrports, and runs a Solr query against thedrupalcore. Thecliservice waits for it to exit with exit code0, so the stack is ready after it exits.
Networks
The networks section declares amazeeio-network, an external network
that Pygmy creates. Only nginx joins it, next to the
project's default network, so Pygmy's proxy can route requests for the
local URL to the web server.
The other services use only the default network that Docker Compose
creates for the project. On that network, the containers reach each other
by service name, such as database or redis. CI deletes the
amazeeio-network lines, which are marked ###, because CI doesn't route
requests through Pygmy.
PHP configuration
The cli and php builds copy custom.ini from the service's directory in
.docker/config/ into PHP's conf.d directory. Set PHP ini directives for
a service in its file:
| Service | File | Applies to |
|---|---|---|
cli | .docker/config/cli/custom.ini | Commands that run in the container, such as Composer and Drush |
php | .docker/config/php/custom.ini | Web requests that PHP-FPM serves |
PHP reads conf.d in alphabetical order, so custom.ini loads after
Lagoon's 00-lagoon-php.ini and overrides its defaults. Those defaults
include the values of PHP_* environment variables, such as
PHP_MEMORY_LIMIT. The php file turns off session upload progress: see
PHP settings.
In the cli container, PHP loads
drush/php-ini/drush.ini last, so a
directive set in both files takes its drush.ini value.
The images include custom.ini at build time, so after you change it,
rebuild the images and recreate the containers with ahoy up --build.
Build context and .dockerignore
When Docker builds an image, it sends a build context, the set of files the
build can copy into the image, to the build engine. Vortex controls it
with a .dockerignore file written as a deny list. Every file is part of
the context by default, and the file lists only what must stay out.
The file excludes the following from the image build context:
- Version control, IDE and operating system files (
.git,.idea,.vscode,**/.DS_Store). - Credentials and local overrides (
auth*,.env.local,.ahoy.local.yml,docker-compose.override.yml,web/sites/*/settings.local.php,web/sites/*/services.local.yml). The package token reaches the build through a build secret and a build argument instead. - Host-installed dependencies, which are reinstalled during the build
(
vendor,node_modules,web/themes/**/node_modules). - Composer-generated Drupal directories, which are regenerated during the
build (
web/core,web/libraries,web/modules/contrib,web/profiles/contrib,web/themes/contrib,drush/Commands/contrib). - Content files, test artifacts, caches, logs and compiled theme assets
(
web/sites/*/files,web/sites/simpletest,**/.artifacts,**/.data,**/.logs,**/.phpunit.cache,**/.twig-cs-fixer.cache,web/themes/**/build). - When the matching features are enabled, the Storybook output
(
web/themes/**/*.stories.json,web/themes/**/storybook-static) and the database cache key files that the GitHub Actions workflow writes (db_cache_*).
Keeping these files out of the context does 3 jobs:
- Security: secrets and database dumps never reach an image layer.
- Speed: a smaller context builds faster.
- Correctness: the CLI build copies the whole context into
/appafter its owncomposer install. Avendordirectory from the host would overwrite thevendordirectory that the build installed.
Everything not listed, such as application code, configuration,
composer.json and settings files, is included, so a new file you add
reaches the build with no change to .dockerignore. That includes .env,
which the CLI image copies in so that Composer scripts can read the
project's variables.
Validate docker-compose.yml
After you edit docker-compose.yml, validate it before you run a build.
The following command parses the file and resolves its variables and
anchors, then prints the result, or the error that stopped it:
docker compose -f docker-compose.yml config