# 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

1. Use the [`ddev-addon-template`](<https://github.com/ddev/ddev-addon-template>) repository
2. Click “Use this template” to create your own repository
3. Customize the `install.yaml` file - see the [example](<https://github.com/ddev/ddev-addon-template/blob/main/install.yaml>)
4. Add the [`#ddev-generated`](<#the-ddev-generated-comment>) comment to all files installed by the add-on
5. Test with [Bats](<#bats-testing-framework>)
6. Create a release when ready
7. If you expect to support and maintain the add-on, add the `ddev-get` [topic](<https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/classifying-your-repository-with-topics>) to the add-on’s GitHub repository. This helps developers find the add-on and encourages reporting issues and feedback.

See this [screencast](<https://www.youtube.com/watch?v=fPVGpKGr0f4>) 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 in `ddev add-on` commands
- **`pre_install_actions`**: Scripts executed before files are copied
- **`project_files`**: Files copied to the project’s `.ddev` directory
- **`global_files`**: Files copied to the [global configuration directory](<https://ddev.github.io/ddev/en/stable/users/usage/architecture/#global-files>)
- **`post_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 required
- **`dependencies`**: Other add-ons this add-on depends on
- **`yaml_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:**

```
.ddev/
├── install.yaml
└── myservice/
    └── scripts/
        ├── setup.php
        └── configure.php
```

**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](<https://ddev.github.io/ddev/en/stable/users/extend/custom-docker-services/#customizing-ddev-describe-output>) for full details, and the [`x-ddev` Extension](<https://ddev.github.io/ddev/en/stable/users/extend/custom-docker-services/#x-ddev-extension>) for all supported keys.

### 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
```

### Tools That Update Themselves

If your add-on installs a tool in the web image and the tool updates itself, the update is lost at the next `ddev restart`, and users fall back to the slow `ddev restart --no-cache`. Use one of these approaches instead. Either way, keep the tool’s configuration and credentials apart from the program, as described in [Persisting Changes Across Restarts](<https://ddev.github.io/ddev/en/stable/users/extend/in-container-configuration/#persisting-changes-across-restarts>).

#### Keep the Tool in `ddev-global-cache`

Install the tool at build time as the web container’s user, not as `root`. The script below can’t move files owned by `root`, so it would leave the tool in the home directory and its updates wouldn’t be kept. Switch users around the install, as in [Installing into the home directory](<https://ddev.github.io/ddev/en/stable/users/extend/customizing-images/#installing-into-the-home-directory>):

```
# .ddev/web-build/Dockerfile.mytool
#ddev-generated
USER $username
RUN curl -fsSL https://example.com/mytool/install.sh | bash
USER root
```

Then add a `web-entrypoint.d/mytool.sh` script to `project_files`. It’s the script from [Persisting Home Subdirectories in `ddev-global-cache`](<https://ddev.github.io/ddev/en/stable/users/extend/in-container-configuration/#persisting-home-subdirectories-in-ddev-global-cache>) with one change: `cp -an` adds files from a newer image but never replaces the stored ones, so the tool’s own updates are kept. This example is for a tool installed in `~/.mytool`:

```
# .ddev/web-entrypoint.d/mytool.sh
#ddev-generated
# Keep mytool in ddev-global-cache so its own updates survive restarts.
# cp -n adds files from a newer image but keeps the stored ones.
(
  for dir in .mytool; do
    target="/mnt/ddev-global-cache/mytool/${HOSTNAME}/${dir}"
    mkdir -p "${target}" "$(dirname ~/"${dir}")" &&
      if [ -d ~/"${dir}" ] && [ ! -L ~/"${dir}" ]; then
        cp -an ~/"${dir}"/. "${target}"/ && rm -rf ~/"${dir}"
      fi &&
      ln -sfn "${target}" ~/"${dir}" ||
      echo "mytool.sh: unable to keep ~/${dir}" >&2
  done
)
```

The first start copies the tool from the image, and after that the tool’s updater writes to the volume. Starting needs no network, and `ddev delete` removes the copy. Keep `${HOSTNAME}` directly under your add-on’s directory, because `ddev delete` doesn’t look deeper. Only keep directories that belong to your tool: if two add-ons link a shared directory like `~/.local/bin`, the last one wins. [ddev-nvm](<https://github.com/ddev/ddev-nvm>) also keeps `~/.nvm` in `ddev-global-cache/nvm_dir/${HOSTNAME}`, but it copies from the image only on the first start, so a newer image doesn’t add anything.

To go back to the version in the image, users can run:

```
ddev exec 'rm -rf /mnt/ddev-global-cache/mytool/${HOSTNAME}'
ddev restart
```

#### Pin the Version

Set the version in an `ARG` and release a new add-on version for each tool update. After `ddev add-on get`, a plain `ddev restart` [rebuilds only from that step on](<https://ddev.github.io/ddev/en/stable/users/extend/customizing-images/#rebuilding-only-what-changed>). Turn off the tool’s own updater, because its updates would be lost.

### Environment Variables

Users configure an add-on through the [env files](<https://ddev.github.io/ddev/en/stable/users/configuration/environment-variables/index.md>) named after it, so `.ddev/.env.myaddon` reaches the `myaddon` service and, with DDEV v1.25.4+, the add-on’s own install actions as well.

To add variables to a service you don’t own, such as `web`, use a labeled file named after your add-on:

```
ddev dotenv set .ddev/.env.web.myaddon --api-url=https://example.com
```

That reaches the `web` container, and two add-ons doing this leave each other’s file alone, which is not the case when both write `.ddev/.env.web`.

Ask for a token or any other secret in a `.local` file, like `.ddev/.env.myaddon.local`, which DDEV keeps out of Git.

> [!WARNING]
>
> **Require v1.25.4 for labeled and `.local` files**
>
> Older DDEV versions don’t raise an error on `.ddev/.env.<service>.<label>` or `.local` files, they simply never read them, so the add-on installs cleanly and misbehaves later. If yours ships or reads one, say so in `install.yaml`:
>
> ```
> ddev_version_constraint: '>= v1.25.4'
> ```

### Version Constraints

Specify minimum DDEV version requirements:

```
ddev_version_constraint: '>= v1.25.2'
```

### 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:

1. **Automatically install it** using the same formats supported by `ddev add-on get`
2. **Detect circular dependencies** and prevent infinite loops
3. **Install recursively** - dependencies of dependencies are also installed

To skip automatic dependency installation, use the `--skip-deps` flag:

```
ddev add-on get --skip-deps my-addon
```

This does not install dependencies. Ensure required dependencies are present if your add-on relies on them.

### Runtime Dependencies (Advanced)

> [!WARNING]
>
> **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

1. **Detection Phase**: During pre-install or post-install actions, your add-on analyzes the project
2. **Creation Phase**: Your add-on creates a `.runtime-deps-<addon-name>` file listing discovered dependencies
3. **Processing Phase**: After installation completes, DDEV automatically processes runtime dependencies
4. **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**:

1. Pre-install actions execute
2. Project files are copied
3. Global files are copied
4. Post-install actions execute
5. **Runtime dependencies are processed** ← This happens last
6. 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](<https://github.com/ddev/ddev-upsun>) add-on demonstrates runtime dependencies by:

1. **Analyzing** `.upsun/config.yaml` during post-install
2. **Detecting** services like Redis, Elasticsearch, Memcached
3. **Creating** runtime dependencies for corresponding DDEV add-ons
4. **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:

```
ddev add-on get --verbose your-addon
```

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:

```
project_files:
  - "settings.${DDEV_PROJECT}.php"
```

### 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:

1. **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-generated` that are listed in the `project_files` or `global_files` will be automatically removed on `ddev add-on remove` if the `#ddev-generated` is found in the file.
2. **Enable clean removal** - `ddev add-on remove` deletes 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`](<#core-sections>) section is the place to clean up any such files.

```
# docker-compose.myservice.yaml
#ddev-generated
services:
  myservice:
    image: myimage:latest
```

In PHP actions, include it as the first line of generated files:

```
file_put_contents('docker-compose.myservice.yaml', "#ddev-generated\n" . yaml_emit($config));
```

In JSON files, add it as a property:

```
{
    "#ddev-generated": true
}
```

### Description Display

Add descriptions to your actions:

```
#ddev-description: Installing Redis configuration
```

```
<?php
#ddev-description: Processing project settings
?>
```

### 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
```

### Interactive Actions

Add `#ddev-interactive` to a Bash action that needs direct terminal input or live output. DDEV runs these actions interactively only when stdin is a terminal and JSON output is disabled. In non-interactive environments, such as CI, DDEV keeps the standard buffered action behavior.

```
post_install_actions:
  - |
    #ddev-description: Configure API credentials
    #ddev-interactive
    if [ "$(ddev dotenv get .ddev/.env.myaddon.local --api-token 2>/dev/null)" = "" ]; then
      read -p "Enter your API token: " TOKEN
      ddev dotenv set .ddev/.env.myaddon.local --api-token="${TOKEN}"
    fi
```

## 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:

```
bats ./tests/test.bats --filter-tags '!release'
```

### Manual Testing

1. Create a test DDEV project
2. Install your add-on locally:

```
ddev add-on get /path/to/your/addon
```

1. Verify services start correctly
2. Test configuration options
3. Test removal process

## Publishing Your Add-on

### Repository Setup

1. **Test thoroughly** using the test framework
2. **Create proper releases** with semantic versioning
3. If you expect the add-on to be maintained and useful to others, **add the `ddev-get` [topic](<https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/classifying-your-repository-with-topics>)** to your GitHub repository.
4. **Write clear documentation** in your readme
5. **Include examples** and configuration options

### Making it Official

To become an officially supported add-on:

1. Open an issue in the [DDEV repository](<https://github.com/ddev/ddev/issues/new>)
2. Request upgrade to official status
3. Commit to maintaining the add-on
4. Subscribe to repository activity and be responsive

### Keeping Your Add-on Up to Date

The [`ddev-addon-template`](<https://github.com/ddev/ddev-addon-template>) evolves over time - workflows, test scripts, tooling, and other components get updated. Run the [`ddev utility addon-update-checker`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#utility-addon-update-checker>) command regularly from your add-on directory (or a workspace containing multiple add-ons) to check whether your add-on is up to date with the template and current best practises:

```
ddev utility addon-update-checker
```

### Best Practices

- **Follow semantic versioning** for releases
- **Maintain backward compatibility** when possible
- **Test with different DDEV versions**
- **Update dependencies regularly** - use `ddev utility addon-update-checker` to keep scripts and tooling in sync with the template
- **Respond to user issues promptly**
- **Keep documentation up to date**
- **Use namespaced directories** (e.g., `myservice/scripts/` not just `scripts/`)
- **Expect changes inside the container to be lost on restart**, see [Tools That Update Themselves](<#tools-that-update-themselves>)

## Examples and References

- **Add-on Template**: [ddev-addon-template](<https://github.com/ddev/ddev-addon-template>)
- **Official Add-ons**: Browse examples at [addons.ddev.com](<https://addons.ddev.com/>)
- **Redis Add-on**: [ddev-redis](<https://github.com/ddev/ddev-redis>) - Example of add-on with `removal_actions` and namespaced scripts directory

## Getting Help

- **Maintenance Guide**: [DDEV Add-on Maintenance Guide](<https://ddev.com/blog/ddev-add-on-maintenance-guide/>)
- **DDEV Discord**: Join [DDEV Discord](<https://ddev.com/s/discord>) for development support
- **GitHub Issues**: Use [DDEV Issues](<https://github.com/ddev/ddev/issues>) for problems and questions
- **Add-on Trainings**: [DDEV Add-ons: Creating, maintaining, testing](<https://www.youtube.com/watch?v=OtmVJtwsHMg>) (YouTube) and [Advanced Add-On Techniques](<https://ddev.com/blog/advanced-add-on-contributor-training/>)

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.
