Custom Docker Compose Services
July 1, 2026 ยท View on GitHub
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.
!!!tip "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, consider converting it to an add-on using the DDEV Add-on Template.
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.
Creating Custom Services
Create docker-compose.*.yaml files in your project's .ddev directory. DDEV automatically processes any files matching this pattern and merges them into the full docker-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"
ports:
- "8080"
environment:
- VIRTUAL_HOST=${DDEV_HOSTNAME}
- HTTP_EXPOSE=8080:8080
- HTTPS_EXPOSE=8081:8080
volumes:
- ".:/mnt/ddev_config"
Service Configuration Best Practices
Required Labels
Always include these labels for proper DDEV integration:
# These two labels are added automatically since DDEV v1.25.2+
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: ${DDEV_APPROOT}
Container Naming
Use consistent naming with the DDEV project:
container_name: "ddev-${DDEV_SITENAME}-servicename"
Restart Policy
Set restart policy to prevent issues:
restart: "no"
Port Exposure
For HTTP services that should be accessible via ddev-router:
ports:
- "8080" # Expose port to Docker network
environment:
- VIRTUAL_HOST=${DDEV_HOSTNAME}
- HTTP_EXPOSE=8080:8080 # HTTP access
- HTTPS_EXPOSE=8081:8080 # HTTPS access
For direct port binding (can cause conflicts between projects):
ports:
- "9999:9999" # Bind to host port 9999
Volume Mounts
Mount your .ddev directory for configuration access:
volumes:
- ".:/mnt/ddev_config"
Mount project files if needed:
volumes:
- "../:/var/www/html:cached"
x-ddev Extension
The x-ddev extension field lets you customize DDEV behavior per service in your .ddev/docker-compose.*.yaml files.
| Key | Description |
|---|---|
describe-url-port | Text shown in the URL/PORT column of ddev describe |
describe-info | Text shown in the INFO column of ddev describe |
ssh-shell | Shell used by ddev ssh -s <service> for this service |
omit-ddev-labels | Skip injecting com.ddev.* labels onto 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 configuration for Changing ddev ssh Shell.
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
SQL Server Database Service
This example shows a custom SQL Server database service, useful when you need a database not natively supported by DDEV.
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"
!!!note "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
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"
ports:
- "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
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"
ports:
- "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"
ports:
- "11211"
Environment Variables and Configuration
Available DDEV Variables
Here is a compressed list of commonly used variables in your service definitions:
${DDEV_SITENAME}- Project name${DDEV_HOSTNAME}- Comma-separated list of FQDN hostnames${DDEV_TLD}- Default top-level domain (ddev.site)${DDEV_APPROOT}- Full path to project root${DDEV_DOCROOT}- Document root (relative to project root)${DDEV_PHP_VERSION}- PHP version${DDEV_WEBSERVER_TYPE}- Web server type${DDEV_DATABASE_FAMILY}- Database family (mysql,postgres)
For a full list, please see Environment Variables Provided.
Custom Environment Variables
Define project-specific variables in .ddev/.env:
MYSERVICE_VERSION=latest
MYSERVICE_MEMORY=512m
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:
ddev dotenv set .ddev/.env.myservice --memory-limit 1024m --debug-mode true
Testing and Debugging Services
Check Service Status
ddev logs myservice
Verify Configuration
ddev utility compose-config
This shows the complete merged docker-compose configuration.
Connect to Service
ddev exec --service=myservice bash
Network Connectivity
Test connectivity from the web container:
ddev exec "curl -s http://myservice:8080/health"
Service Integration Patterns
Database Integration
Add database connection info to web container in .ddev/docker-compose.web-env.yaml:
services:
web:
environment:
- MYDB_HOST=mydb
- MYDB_PORT=5432
- MYDB_DATABASE=myproject
- MYDB_USER=db
- MYDB_PASSWORD=db
Configuration File Mounting
Mount configuration from your project in .ddev/docker-compose.config.yaml:
services:
myservice:
volumes:
- "./config/myservice.conf:/etc/myservice/myservice.conf:ro"
Initialization Scripts
Run initialization scripts in .ddev/docker-compose.init.yaml:
services:
myservice:
volumes:
- "./scripts/init.sh:/docker-entrypoint-initdb.d/init.sh:ro"
Troubleshooting
Common Issues
Port conflicts: Multiple projects using the same service may conflict. Use project-specific ports or let DDEV handle routing.
Service won't start: Check ddev logs servicename for error messages.
Network connectivity: Ensure services are on the same Docker network (automatic with DDEV).
File permissions: Use appropriate volume mount options (:cached, :ro).
Debugging Steps
- Verify syntax:
ddev utility compose-config - Check logs:
ddev logs servicename - Test connectivity:
ddev exec "ping servicename" - Inspect container:
ddev exec --service=servicename bash
Migration from ddev-contrib
Many services previously documented in ddev-contrib have been converted to official add-ons. Check DDEV Add-on Registry first before creating custom services.
Still Available in ddev-contrib
- Old PHP Versions: Old PHP Versions
- Specialized configurations: Various experimental and niche services
Best Practices
Performance
- Use specific image tags instead of
latest - Set appropriate resource limits
- Use volume caching options (
:cached) - Minimize container layers and size
Security
- Don't expose unnecessary ports to the host
- Use non-root users when possible
Maintainability
- Document your service configuration
- Use meaningful container names
- Group related services in single files
- Comment complex configurations
Team Sharing
- Include service documentation in your project readme
- Use environment variables for customizable values
- Provide setup and testing instructions
- Consider creating an add-on for reusable services
Converting to Add-ons
If your custom service becomes stable and useful for multiple projects, consider converting it to a DDEV add-on. This allows you to:
- Share the service with your team across projects
- Contribute to the DDEV community
- Benefit from automatic installation and configuration
- Add version management and updates
Steps to convert:
- Create an add-on repository from DDEV Add-on Template
- Move your service configuration to the add-on
- Add installation actions and configuration options
- Create tests and documentation
- Publish and share with the community
See Creating Add-ons for detailed instructions.
Custom Docker Compose services provide the ultimate flexibility for customizing your DDEV environment. While add-ons are recommended for common services, custom services let you integrate exactly what your project needs, with the potential to evolve into shareable add-ons.