Custom Docker Compose Services¶
When you need services that aren’t available as DDEV add-ons, or require deep customization beyond what add-ons provide, you can create custom Docker Compose services using docker-compose.*.yaml files. For how DDEV merges these files and the conventions your service should follow (labels, container naming, the build:/-built tag pattern, port exposure), see Defining Additional Services with Docker Compose.
From Custom Services to Add-ons
Many successful custom services eventually become DDEV add-ons so they can be shared with teams, between projects, or with the broader community. If you find your custom service useful and stable, convert it to an add-on using the DDEV Add-on Template and see Creating Add-ons.
When to Use Custom Services¶
Use custom Docker Compose services when:
- You need a custom or highly specialized service
- You require deep customization of service configuration
- You’re prototyping or experimenting with service configurations
- The service doesn’t justify creating a full add-on yet
- You need tight integration with your specific project setup
Use add-ons when:
- An add-on is already available that provides a standard, tested service (Redis, Elasticsearch, Solr)
- You want automatic configuration and setup
See Using Add-ons for pre-built add-ons.
Whichever you choose, the same conventions apply once you have a compose file — see Defining Additional Services with Docker Compose.
Creating Custom Services¶
A custom service typically runs a container based on a Docker image and provides a specific “service”. It’s defined in a .ddev/docker-compose.*.yaml. DDEV automatically processes any files matching this pattern and merges them into the full Compose configuration.
Basic Service Example¶
Create .ddev/docker-compose.myservice.yaml:
services:
myservice:
container_name: "ddev-${DDEV_SITENAME}-myservice"
image: nginx:alpine
# These two labels are added automatically since DDEV v1.25.2+
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: ${DDEV_APPROOT}
restart: "no"
expose:
- "8080"
environment:
- VIRTUAL_HOST=${DDEV_HOSTNAME}
- HTTP_EXPOSE=8080:8080
- HTTPS_EXPOSE=8081:8080
volumes:
- ".:/mnt/ddev_config"
Service Configuration Best Practices¶
Required labels, container naming, restart policy, and HTTP vs. direct port binding are covered in Conventions for Defining Additional Services — follow those for every custom service.
Volume Mounts¶
Provide a bind-mount of your .ddev directory for configuration access:
Mount project files if needed:
x-ddev Extension¶
The x-ddev extension field lets you customize DDEV behavior per service in your .ddev/docker-compose.*.yaml files.
| Key | Description |
|---|---|
container-user | User that ddev ssh, ddev exec, and custom commands run as for this service |
describe-info | Text shown in the INFO column of ddev describe |
describe-url-port | Text shown in the URL/PORT column of ddev describe |
omit-ddev-labels | Skip injecting com.ddev.* labels onto this service |
pull-images | Images DDEV pulls before building this service |
ssh-shell | Shell used by ddev ssh -s <service> for this service |
Customizing ddev describe Output¶
You can use the x-ddev extension field in your .ddev/docker-compose.*.yaml configuration to customize the output of ddev describe.
This feature is useful for showing credentials, URLs, or usage notes for custom services.
services:
rabbitmq:
container_name: "ddev-${DDEV_SITENAME}-rabbitmq"
image: rabbitmq:3-management-alpine
# These two labels are added automatically since DDEV v1.25.2+
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: ${DDEV_APPROOT}
restart: "no"
expose:
- "15672"
environment:
- VIRTUAL_HOST=${DDEV_HOSTNAME}
- HTTP_EXPOSE=15672:15672
- HTTPS_EXPOSE=15673:15672
- RABBITMQ_DEFAULT_USER=rabbitmq
- RABBITMQ_DEFAULT_PASS=rabbitmq
x-ddev:
# Can be multi-line block
describe-info: |
User: rabbitmq
Pass: rabbitmq
# Or single line string
describe-url-port: "extra help here"
x-ddev.describe-url-port: Appears in theURL/PORTcolumn when runningddev describe.x-ddev.describe-info: Appears in theINFOcolumn, making it easy for team members to view relevant service details without checking config files.
Tip
See related x-ddev.ssh-shell and x-ddev.container-user configuration for Changing ddev ssh Shell and Changing the Container User.
Omitting com.ddev.* Labels from a Service¶
Setting x-ddev.omit-ddev-labels: true stops DDEV from injecting com.ddev.* labels onto a service.
DDEV normally stamps these labels (like com.ddev.site-name) on every service and has ddev start wait for the matching containers. That makes ddev start fail when a one-shot container exits before the wait completes. Omitting the labels drops the service from that wait, while leaving it in the compose file so ddev stop, ddev poweroff, and ddev delete still tear it down.
services:
# One-shot container: prepares the shared volume, then exits
init:
container_name: "ddev-${DDEV_SITENAME}-init"
image: busybox:stable
command:
- sh
- -c
- |
mkdir -p /mnt/assets/cache &&
chown -R ${DDEV_UID}:${DDEV_GID} /mnt/assets
volumes:
- "assets:/mnt/assets"
x-ddev:
omit-ddev-labels: true
# web mounts the same volume
web:
volumes:
- "assets:/mnt/assets"
depends_on:
init:
condition: service_completed_successfully
volumes:
assets:
Advanced Service Examples¶
Service with a Custom Build¶
If the stock image needs an extra tool or package, add a build: section instead of a plain image:. Follow the -${DDEV_SITENAME}-<servicename>-built tag convention from Conventions for Defining Additional Services.
Create .ddev/docker-compose.myservice.yaml:
services:
myservice:
container_name: "ddev-${DDEV_SITENAME}-myservice"
image: ${BASE_IMAGE:-nginx:stable}-${DDEV_SITENAME}-myservice-built
build:
dockerfile_inline: |
ARG BASE_IMAGE="scratch"
FROM $${BASE_IMAGE}
RUN apt-get update && apt-get install -y curl
args:
BASE_IMAGE: ${BASE_IMAGE:-nginx:stable}
# These two labels are added automatically since DDEV v1.25.2+
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: ${DDEV_APPROOT}
restart: "no"
To pull other images before the build, such as a FROM written in the Dockerfile, list them in x-ddev.pull-images:
SQL Server Database Service¶
This example shows a custom SQL Server database service, useful when you need a database not natively supported by DDEV.
A maintained add-on already exists
This example grew into the ddev-sqlsrv add-on. Its sqlsrv service skips host ports: entirely and is reached only from the web container over the Docker network, so it avoids the port conflict noted below — use that pattern instead of the one here unless you specifically need to connect from the host.
Create .ddev/docker-compose.sqlsrv.yaml:
services:
sqlsrv:
container_name: "ddev-${DDEV_SITENAME}-sqlsrv"
image: mcr.microsoft.com/mssql/server:2022-latest
# These two labels are added automatically since DDEV v1.25.2+
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: ${DDEV_APPROOT}
restart: "no"
ports:
- "1433:1433" # Direct port binding for SQL Server protocol
environment:
- SA_PASSWORD=Password123!
- ACCEPT_EULA=Y
- MSSQL_PID=Express
volumes:
- "sqlsrv-data:/var/opt/mssql"
- ".:/mnt/ddev_config"
# Platform specification for ARM64 compatibility
platform: linux/amd64
volumes:
sqlsrv-data:
external: true
name: "${DDEV_SITENAME}-sqlsrv-data"
Non-HTTP Services Require Direct Port Binding
SQL Server uses a proprietary protocol that cannot be routed through the DDEV router, so it requires direct ports binding. This means only one project can use SQL Server at a time unless you change the port.
Service with Custom Configuration¶
A maintained add-on already exists
This example grew into the ddev-elasticsearch add-on, which also adds a healthcheck: and supports switching Elasticsearch major versions.
Create .ddev/docker-compose.elasticsearch.yaml:
services:
elasticsearch:
container_name: "ddev-${DDEV_SITENAME}-elasticsearch"
image: elasticsearch:8.11.0
# These two labels are added automatically since DDEV v1.25.2+
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: ${DDEV_APPROOT}
restart: "no"
expose:
- "9200"
environment:
- VIRTUAL_HOST=${DDEV_HOSTNAME}
- HTTP_EXPOSE=9200:9200
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- "elasticsearch-data:/usr/share/elasticsearch/data"
- "./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro"
volumes:
elasticsearch-data:
external: true
name: "${DDEV_SITENAME}-elasticsearch-data"
Multi-Service Setup¶
It’s usually easier and clearer to have a separate docker-compose.*.yaml file for each service, but it’s possible to define more than one in a single file.
Maintained add-ons already exist
These two grew into the ddev-redis and ddev-memcached add-ons. Neither binds a host port at all, since both are reached only from the web container — that’s why expose:, not ports:, is used below.
Create .ddev/docker-compose.cache.yaml:
services:
redis:
container_name: "ddev-${DDEV_SITENAME}-redis"
image: redis:7-alpine
# These two labels are added automatically since DDEV v1.25.2+
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: ${DDEV_APPROOT}
restart: "no"
expose:
- "6379"
memcached:
container_name: "ddev-${DDEV_SITENAME}-memcached"
image: memcached:alpine
# These two labels are added automatically since DDEV v1.25.2+
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: ${DDEV_APPROOT}
restart: "no"
expose:
- "11211"
Environment Variables and Configuration¶
DDEV variables such as ${DDEV_SITENAME} and ${DDEV_APPROOT} are available for interpolation in your service definitions; see Environment Variables Provided for the full list.
Custom Environment Variables¶
Define project-specific variables in .ddev/.env:
Then use in your service:
services:
myservice:
image: myservice:${MYSERVICE_VERSION:-latest}
environment:
- MEMORY_LIMIT=${MYSERVICE_MEMORY:-256m}
Service-Specific Environment Files¶
Use .ddev/.env.servicename for service-specific variables:
Variables from every .ddev/.env* file are available for interpolation here, but only a file named after an existing service sets variables inside that container. See Environment Variables for the file naming rules, the global $HOME/.ddev/.env* equivalents, and where to keep a secret.
Debugging a Custom Service¶
ddev logs --service myservice and ddev exec --service myservice bash cover most cases; see Interacting with Additional Services. To see how DDEV merged your compose files, run ddev utility compose-config.
Custom services let you integrate exactly what your project needs, with the potential to evolve into a shareable add-on — see the tip at the top of this page.