Adding a Custom Python Service to DDEV

A quick guide on adding a local machine learning microservice to your DDEV environment, reachable from your app container over HTTP.

The pattern is useful when you want to keep ML workloads in a separate process — a stateless HTTP service that your app calls directly in development, while in production the same logic runs as a queue worker.

Create ml-service/Dockerfile

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001", "--reload"]

Create .ddev/docker-compose.ml.yaml

services:
  ml:
    container_name: ddev-${DDEV_SITENAME}-ml
    image: ${DDEV_SITENAME}-ml-service
    build:
      context: ../ml-service
    labels:
      com.ddev.site-name: ${DDEV_SITENAME}
      com.ddev.approot: $DDEV_APPROOT
    expose:
      - "8001"
    environment:
      - VIRTUAL_HOST=$DDEV_HOSTNAME
    networks:
      - ddev_default
    restart: "no"

The build.context points to the ml-service/ directory at the repo root. DDEV builds the image on ddev restart.

Create .ddev/commands/host/ml

#!/bin/bash
## Description: Run a command in the ml container
## Usage: ml [command]
## Example: ddev ml python -c "import sklearn; print(sklearn.__version__)"

docker exec -it ddev-${DDEV_SITENAME}-ml "$@"

Make it executable:

chmod +x .ddev/commands/host/ml

Restart DDEV

ddev restart

DDEV will build the image on first start. Subsequent restarts reuse it unless the Dockerfile or requirements.txt changes.

Verify the service is running

ddev exec curl -s http://ddev-nf-ml:8001/docs

The service is reachable from the web container at http://ddev-{SITENAME}-ml:8001.

Calling the service from your app

From inside the web container (e.g. your PHP app), use the container hostname:

http://ddev-{SITENAME}-ml:8001

No credentials needed — the service is only accessible within the DDEV network.

Note: The service is stateless by design. Your app fetches the data it needs from its own database and passes it in the request body. The ML container never touches the database — a deliberate architectural boundary that keeps local and production behaviour consistent.