Creating DDEV Add-ons¶
DDEV add-ons provide a powerful way to extend development environments. You can create add-ons using traditional Bash actions or the new PHP-based actions for complex configuration processing.
Quick Start¶
- Use the
ddev-addon-templaterepository - Click “Use this template” to create your own repository
- Customize the
install.yamlfile - see the example - Add the
#ddev-generatedcomment to all files installed by the add-on - Test with Bats
- Create a release when ready
- If you expect to support and maintain the add-on, add the
ddev-gettopic to the add-on’s GitHub repository. This helps developers find the add-on and encourages reporting issues and feedback.
See this screencast for a walkthrough.
Add-on Structure¶
Every add-on requires an install.yaml file with these sections:
name: my-addon
pre_install_actions: []
project_files: []
global_files: []
post_install_actions: []
removal_actions: []
Core Sections¶
name: The add-on name used inddev add-oncommandspre_install_actions: Scripts executed before files are copiedproject_files: Files copied to the project’s.ddevdirectoryglobal_files: Files copied to the global configuration directorypost_install_actions: Scripts executed after files are copied-
removal_actions: Scripts executed when removing the add-on. Use these to clean up files that don’t carry#ddev-generated(e.g. files placed outside.ddev/, or files the user may have modified).removal_actions: - | #ddev-description: Remove generated settings file if unmodified if [ -f "${DDEV_APPROOT}/${DDEV_DOCROOT}/sites/default/settings.ddev.myservice.php" ]; then if grep -q '#ddev-generated' "${DDEV_APPROOT}/${DDEV_DOCROOT}/sites/default/settings.ddev.myservice.php"; then rm -f "${DDEV_APPROOT}/${DDEV_DOCROOT}/sites/default/settings.ddev.myservice.php" else echo "Skipping removal: settings.ddev.myservice.php has been modified." fi fi
Advanced Sections¶
ddev_version_constraint: Minimum DDEV version requireddependencies: Other add-ons this add-on depends onyaml_read_files: YAML files to read for template processing
Action Types: Bash vs PHP¶
Traditional Bash Actions¶
Bash actions run directly on the host system and are suitable for:
- File permissions and system commands
- Environment setup and package installation
- Direct command execution
- Simple file operations
name: bash-example
post_install_actions:
- |
#ddev-description: Configure project settings
echo "Setting up project: $DDEV_PROJECT"
chmod +x .ddev/commands/web/mycommand
PHP-based Actions ✨ NEW¶
PHP actions provide powerful capabilities for:
- Complex data processing and YAML manipulation
- Conditional logic based on project configuration
- Cross-platform compatibility
- File content generation and template processing
Why Use PHP Actions?¶
- Better YAML processing with the built-in php-yaml extension
- Cross-platform compatibility (no shell scripting differences)
- Rich string manipulation and data processing capabilities
- Access to DDEV project configuration through environment variables
- Familiar syntax for developers working with PHP projects
Basic PHP Action¶
name: php-example
post_install_actions:
- |
<?php
#ddev-description: Process project configuration
// Access DDEV environment variables
$projectName = $_ENV['DDEV_PROJECT'];
$projectType = $_ENV['DDEV_PROJECT_TYPE'];
$docroot = $_ENV['DDEV_DOCROOT'];
echo "Setting up $projectType project: $projectName\n";
// Generate YAML configuration
$config = [
'services' => [
'myservice' => [
'image' => 'nginx:latest',
'environment' => [
'PROJECT_TYPE' => $projectType
]
]
]
];
file_put_contents('docker-compose.myservice.yaml',
"#ddev-generated\n" . yaml_emit($config));
?>
Available Environment Variables¶
PHP actions have access to all standard DDEV environment variables:
<?php
// Project Information
$_ENV['DDEV_PROJECT'] // Project name
$_ENV['DDEV_PROJECT_TYPE'] // 'drupal', 'wordpress', 'laravel', etc.
$_ENV['DDEV_APPROOT'] // '/var/www/html' (project root)
$_ENV['DDEV_DOCROOT'] // 'web', 'public', or configured docroot
$_ENV['DDEV_TLD'] // 'ddev.site' or configured TLD
// Technology Stack
$_ENV['DDEV_PHP_VERSION'] // '8.1', '8.2', '8.3', etc.
$_ENV['DDEV_WEBSERVER_TYPE'] // 'nginx-fpm', 'apache-fpm'
$_ENV['DDEV_DATABASE'] // 'mysql:8.0', 'postgres:16', etc.
$_ENV['DDEV_DATABASE_FAMILY'] // 'mysql', 'postgres'
// System Information
$_ENV['DDEV_VERSION'] // Current DDEV version
$_ENV['DDEV_MUTAGEN_ENABLED'] // 'true' or 'false'
?>
PHP Action Execution Environment¶
PHP actions run inside a temporary container, unlike Bash actions which run on the host.
- Working directory:
/var/www/html/.ddev(your project’s .ddev directory) - Project access: Full read/write access to project repository at
/var/www/html/ - Error handling: Automatic strict error handling (equivalent to Bash
set -eu) - Extensions: php-yaml extension for robust YAML processing
Advanced PHP Example: Conditional Configuration¶
name: conditional-config
pre_install_actions:
- |
<?php
#ddev-description: Generate environment-specific configuration
$projectType = $_ENV['DDEV_PROJECT_TYPE'];
$services = [];
// Different services based on project type
switch($projectType) {
case 'drupal':
$services['redis'] = [
'image' => 'redis:7-alpine',
'ports' => ['6379:6379']
];
break;
case 'wordpress':
$services['memcached'] = [
'image' => 'memcached:alpine',
'ports' => ['11211:11211']
];
break;
default:
$services['cache'] = [
'image' => 'nginx:alpine'
];
}
$composeContent = ['services' => $services];
file_put_contents('docker-compose.conditional.yaml',
"#ddev-generated\n" . yaml_emit($composeContent));
echo "Generated configuration for $projectType project\n";
?>
Separate PHP Script Files (Best Practice)¶
For complex logic, create separate PHP script files using your add-on’s namespace:
File structure:
Clean install.yaml:
name: myservice
project_files:
- myservice/scripts/setup.php
- myservice/scripts/configure.php
post_install_actions:
- |
<?php
#ddev-description: Configure project
require 'myservice/scripts/setup.php';
- |
<?php
#ddev-description: Apply optimizations
require 'myservice/scripts/configure.php';
myservice/scripts/setup.php:
<?php
#ddev-generated
$projectType = $_ENV['DDEV_PROJECT_TYPE'];
$docroot = $_ENV['DDEV_DOCROOT'];
// Exit early if not applicable
if ($projectType !== 'drupal') {
echo "Not a Drupal project, skipping\n";
exit(0);
}
// Perform Drupal-specific setup
$settingsFile = "/var/www/html/{$docroot}/sites/default/settings.ddev.php";
$settings = "<?php\n// DDEV-generated settings\n";
file_put_contents($settingsFile, $settings);
echo "Drupal settings configured\n";
Real-world example: ddev-redis structure¶
.ddev/
├── install.yaml
├── docker-compose.redis.yaml
└── redis/
└── scripts/
├── setup-drupal-settings.php
├── setup-redis-optimized-config.php
└── settings.ddev.redis.php
Mixed Bash and PHP Actions¶
You can combine both approaches in a single add-on:
name: mixed-actions
pre_install_actions:
- |
#ddev-description: Set file permissions
chmod +x .ddev/commands/web/mycommand
- |
<?php
#ddev-description: Process configuration
$projectName = $_ENV['DDEV_PROJECT'];
echo "Processing config for: $projectName\n";
?>
Advanced Features¶
Customizing ddev describe Output¶
Add-ons can customize what appears in ddev describe using x-ddev.describe-* extensions in your docker-compose file (requires DDEV v1.24.10+):
services:
myservice:
image: myimage:latest
x-ddev:
describe-url-port: "https://${DDEV_HOSTNAME}:8080"
describe-info: "API key: ${MYSERVICE_API_KEY}"
See Customizing ddev describe output for full details.
Minor Docker Image Customization¶
For small tweaks to an existing image, use dockerfile_inline instead of maintaining a separate Dockerfile:
services:
myservice:
image: myimage:latest
build:
dockerfile_inline: |
FROM myimage:latest
RUN apt-get update && apt-get install -y mypackage
Version Constraints¶
Specify minimum DDEV version requirements:
Dependencies¶
Declare add-on dependencies that will be automatically installed:
dependencies:
- ddev/ddev-redis # GitHub repository
- https://example.com/addon.tar.gz # Direct tarball URL
Dependencies are automatically installed when the add-on is installed. If a dependency is missing, DDEV will:
- Automatically install it using the same formats supported by
ddev add-on get - Detect circular dependencies and prevent infinite loops
- Install recursively - dependencies of dependencies are also installed
To skip automatic dependency installation, use the --skip-deps flag:
This does not install dependencies. Ensure required dependencies are present if your add-on relies on them.
Runtime Dependencies (Advanced)¶
Advanced Feature
Runtime dependencies are an advanced, rarely-used feature for sophisticated add-ons that need to dynamically discover dependencies during installation. Most add-ons should use static dependencies declared in install.yaml instead.
Runtime dependencies allow add-ons to dynamically discover and install dependencies during the installation process, rather than declaring them statically. This enables complex scenarios like:
- Service detection - Analyzing project configuration to determine needed services
- Conditional dependencies - Installing different add-ons based on project analysis
- Dynamic configuration processing - Dependencies determined by parsing external files
How Runtime Dependencies Work¶
- Detection Phase: During pre-install or post-install actions, your add-on analyzes the project
- Creation Phase: Your add-on creates a
.runtime-deps-<addon-name>file listing discovered dependencies - Processing Phase: After installation completes, DDEV automatically processes runtime dependencies
- Installation Phase: DDEV installs any missing dependencies and cleans up the runtime dependencies file
Creating Runtime Dependencies¶
Create a .runtime-deps-<addon-name> file in the project’s .ddev directory with one dependency per line:
Example: Dynamic service detection in post-install action¶
name: my-dynamic-addon
post_install_actions:
- |
<?php
#ddev-description: Detect required services dynamically
$services = [];
// Analyze project configuration
if (file_exists('/var/www/html/.platform.yaml')) {
$config = yaml_parse_file('/var/www/html/.platform.yaml');
// Check for Redis usage
if (isset($config['services']['cache']['type']) &&
strpos($config['services']['cache']['type'], 'redis') !== false) {
$services[] = 'ddev/ddev-redis';
}
// Check for Elasticsearch usage
if (isset($config['services']['search']['type']) &&
strpos($config['services']['search']['type'], 'elasticsearch') !== false) {
$services[] = 'ddev/ddev-elasticsearch';
}
}
// Create runtime dependencies file if services were found
if (!empty($services)) {
$runtimeDepsFile = '.runtime-deps-my-dynamic-addon';
file_put_contents($runtimeDepsFile, implode("\n", $services) . "\n");
echo "Created runtime dependencies for " . count($services) . " service(s)\n";
}
?>
Runtime Dependencies File Format¶
The .runtime-deps-<addon-name> file uses the same dependency formats as static dependencies:
# One dependency per line
ddev/ddev-redis
ddev/ddev-elasticsearch
https://example.com/addon.tar.gz
# Comments and empty lines are ignored
Processing Timing¶
Runtime dependencies are processed after all installation phases complete:
- Pre-install actions execute
- Project files are copied
- Global files are copied
- Post-install actions execute
- Runtime dependencies are processed ← This happens last
- Cleanup occurs
This timing ensures that:
- Add-ons can analyze the fully installed project state
- Post-install actions can create runtime dependencies based on project configuration
- Dependencies have access to all project files when they install
Real-world Example: Upsun Integration¶
The ddev-upsun add-on demonstrates runtime dependencies by:
- Analyzing
.upsun/config.yamlduring post-install - Detecting services like Redis, Elasticsearch, Memcached
- Creating runtime dependencies for corresponding DDEV add-ons
- Automatically installing the required service add-ons
<?php
// Simplified example from ddev-upsun
$detectedServices = analyzeUpsunConfig('/var/www/html/.upsun/config.yaml');
$dependencies = [];
foreach ($detectedServices as $service) {
switch ($service['type']) {
case 'redis':
$dependencies[] = 'ddev/ddev-redis';
break;
case 'opensearch':
$dependencies[] = 'ddev/ddev-opensearch';
break;
}
}
if (!empty($dependencies)) {
file_put_contents('.runtime-deps-upsun', implode("\n", $dependencies));
}
?>
When NOT to Use Runtime Dependencies¶
Use static dependencies instead if:
- Dependencies are always required
- Dependencies don’t change based on project analysis
- You want simpler, more predictable behavior
Use runtime dependencies only if:
- Dependencies must be determined by analyzing project files
- Different projects need different dependencies
- You’re integrating with external platform configurations
Debugging Runtime Dependencies¶
Add verbose logging to debug runtime dependency processing:
This will show:
- When runtime dependencies files are created
- What dependencies are discovered
- Installation progress for each dependency
Limitations¶
- Runtime dependencies cannot create circular dependency loops
- The
.runtime-deps-*file is automatically deleted after processing - Runtime dependencies are not processed when using
--skip-deps - Cannot be used to conditionally install the add-on itself
Template Replacements (Advanced, Very Unusual)¶
Use environment variables in filenames and content:
YAML File Processing¶
(Using YAML file processing is very unusual.)
yaml_read_files reads external YAML files and makes their contents available as Go template variables in Bash actions. This is a Go template feature and cannot be used inside PHP actions.
yaml_read_files:
platformapp: ".platform.app.yaml"
services: ".platform/services.yaml"
post_install_actions:
- |
#ddev-description: Configure PHP version from platform config
cat <<EOF >${DDEV_APPROOT}/.ddev/config.platformsh.yaml
php_version: {{ trimPrefix "php:" .platformapp.type }}
EOF
Error Handling¶
Use proper exit codes and error messages:
<?php
#ddev-description: Validate requirements
if (empty($_ENV['DDEV_PROJECT'])) {
echo "Error: DDEV environment not available\n";
exit(1);
}
// Continue with setup...
echo "Requirements validated\n";
?>
Special Directives¶
The #ddev-generated Comment¶
Add #ddev-generated as a comment in any file your add-on creates or copies into a project. DDEV uses this marker to:
- Track managed files - on a subsequent
ddev add-on get, DDEV replaces the file automatically if it contains#ddev-generated, even if the user has modified it. Files with#ddev-generatedthat are listed in theproject_filesorglobal_fileswill be automatically removed onddev add-on removeif the#ddev-generatedis found in the file. - Enable clean removal -
ddev add-on removedeletes all files that still contain#ddev-generated
Files that do not contain this comment are left in place during removal (assumed to be user-modified). The removal_actions section is the place to clean up any such files.
In PHP actions, include it as the first line of generated files:
In JSON files, add it as a property:
Description Display¶
Add descriptions to your actions:
Warning Exit Codes¶
Treat specific exit codes as warnings instead of errors:
post_install_actions:
- |
#ddev-warning-exit-code: 2
#ddev-description: Optional configuration
some-command-that-might-fail
Testing Your Add-on¶
Bats Testing Framework¶
The add-on template includes a tests/test.bats file for testing:
#!/usr/bin/env bats
setup() {
set -eu -o pipefail
# Override this variable for your add-on:
export GITHUB_REPO=owner/repo
# ...
}
health_checks() {
# Do something useful here that verifies the add-on
# You can check for specific information in headers:
run curl -sfI https://${PROJNAME}.ddev.site
assert_output --partial "HTTP/2 200"
assert_output --partial "test_header"
# Or check if some command gives expected output:
DDEV_DEBUG=true run ddev launch
assert_success
assert_output --partial "FULLURL https://${PROJNAME}.ddev.site"
}
@test "install from directory" {
set -eu -o pipefail
echo "# ddev add-on get ${DIR} with project ${PROJNAME} in $(pwd)" >&3
run ddev add-on get "${DIR}"
assert_success
run ddev restart -y
assert_success
health_checks
}
# bats test_tags=release
@test "install from release" {
set -eu -o pipefail
echo "# ddev add-on get ${GITHUB_REPO} with project ${PROJNAME} in $(pwd)" >&3
run ddev add-on get "${GITHUB_REPO}"
assert_success
run ddev restart -y
assert_success
health_checks
}
Run tests in the add-on root directory:
Manual Testing¶
- Create a test DDEV project
- Install your add-on locally:
- Verify services start correctly
- Test configuration options
- Test removal process
Publishing Your Add-on¶
Repository Setup¶
- Test thoroughly using the test framework
- Create proper releases with semantic versioning
- If you expect the add-on to be maintained and useful to others, add the
ddev-gettopic to your GitHub repository. - Write clear documentation in your readme
- Include examples and configuration options
Making it Official¶
To become an officially supported add-on:
- Open an issue in the DDEV repository
- Request upgrade to official status
- Commit to maintaining the add-on
- Subscribe to repository activity and be responsive
Best Practices¶
- Follow semantic versioning for releases
- Maintain backward compatibility when possible
- Test with different DDEV versions
- Update dependencies regularly
- Respond to user issues promptly
- Keep documentation up to date
- Use namespaced directories (e.g.,
myservice/scripts/not justscripts/)
Examples and References¶
- Add-on Template: ddev-addon-template
- Official Add-ons: Browse examples at addons.ddev.com
- Redis Add-on: ddev-redis - Example of add-on with
removal_actionsand namespaced scripts directory
Getting Help¶
- Maintenance Guide: DDEV Add-on Maintenance Guide
- DDEV Discord: Join DDEV Discord for development support
- GitHub Issues: Use DDEV Issues for problems and questions
- Add-on Trainings: DDEV Add-ons: Creating, maintaining, testing (YouTube) and Advanced Add-On Techniques
Creating DDEV add-ons is a powerful way to contribute to the DDEV ecosystem. Whether you use traditional Bash actions or the new PHP-based actions, you can create sophisticated extensions that help developers worldwide.