# How to Customize ODS with User Extensions: A Complete Guide to Service Extensions

> Customize ODS with user extensions. Learn to create self-contained service extensions with manifest.yaml and Docker Compose for seamless integration. A complete guide for Osmantic ODS.

- Repository: [Osmantic/ODS](https://github.com/Osmantic/ODS)
- Tags: how-to-guide
- Published: 2026-09-01

---

**ODS supports customization through self-contained service extensions that combine a [`manifest.yaml`](https://github.com/Osmantic/ODS/blob/main/manifest.yaml) for metadata with a Docker Compose fragment for containerization, enabling seamless integration with the core stack.**

Osmantic/ODS (Open Data Services) provides a modular architecture designed to be extended without modifying core system files. When you customize ODS with user extensions, you create independent packages under `extensions/services/` that the platform discovers, validates, and orchestrates automatically. This approach keeps your core installation stable while allowing you to add specialized AI services, databases, or utility containers on demand.

## Understanding the ODS Extension Architecture

ODS extensions follow a strict two-file structure. Each extension resides in its own subdirectory under `extensions/services/` and contains:

- **A manifest file** ([`manifest.yaml`](https://github.com/Osmantic/ODS/blob/main/manifest.yaml)): Defines service metadata including the unique ID, exposed ports, health check endpoints, GPU compatibility flags, and category classification.
- **A Docker Compose fragment** ([`compose.yaml`](https://github.com/Osmantic/ODS/blob/main/compose.yaml)): Declares the container configuration, networking, volumes, and dependencies on other ODS services like `llama-server`.

At runtime, the **compose stack resolver** ([`scripts/resolve-compose-stack.sh`](https://github.com/Osmantic/ODS/blob/main/scripts/resolve-compose-stack.sh)) aggregates the base infrastructure ([`docker-compose.base.yml`](https://github.com/Osmantic/ODS/blob/main/docker-compose.base.yml) and GPU-specific overlays) with all enabled extension fragments. This merged configuration is handed to the **host agent** ([`bin/ods-host-agent.py`](https://github.com/Osmantic/ODS/blob/main/bin/ods-host-agent.py)), which executes the Docker Compose commands to start or stop containers. Meanwhile, the **Dashboard API** loads extension manifests at startup via `config.py:load_extension_manifests()` and exposes them through the catalog endpoint (`GET /api/extensions/catalog`).

## Creating a New Service Extension

You can add a custom service to ODS in approximately 30 minutes using the provided templates. The process involves scaffolding the directory structure, defining the manifest, and writing the Compose configuration.

First, create the extension directory and copy the starter templates:

```bash
mkdir extensions/services/my-service
cp extensions/templates/service-template.yaml extensions/services/my-service/manifest.yaml
cp extensions/templates/compose-template.yaml extensions/services/my-service/compose.yaml

```

Next, edit the [`manifest.yaml`](https://github.com/Osmantic/ODS/blob/main/manifest.yaml) to specify your service metadata. Key fields include `id` (unique identifier), `port` (external port mapping), `health` (health check path), and `gpu_backends` (array supporting `amd` or `nvidia`):

```yaml
id: my-service
name: My Service
port: 9200
health: /health
category: optional
gpu_backends: [amd, nvidia]

```

Finally, define the [`compose.yaml`](https://github.com/Osmantic/ODS/blob/main/compose.yaml) fragment to configure the container. This example demonstrates environment variable injection, service dependencies, and health check configuration:

```yaml
services:
  my-service:
    image: my-org/my-service:latest
    container_name: ods-my-service
    restart: unless-stopped
    ports:
      - "${MY_SERVICE_PORT:-9200}:8080"
    environment:
      - LLM_URL=http://llama-server:8080/v1
    depends_on:
      llama-server:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 15s

```

## Validating Your Extension Before Deployment

Before activating your extension, validate both the manifest schema and the Docker Compose merge compatibility to prevent runtime errors.

Perform schema validation to ensure your manifest is properly formatted YAML:

```bash
python3 -c "import yaml, sys; yaml.safe_load(open('extensions/services/my-service/manifest.yaml'))"

```

Test how your extension integrates with the full stack using the compose config command. This merges your fragment with the base and GPU overlays to check for port conflicts or dependency errors:

```bash
docker compose -f docker-compose.base.yml \
  -f docker-compose.amd.yml \
  -f extensions/services/my-service/compose.yaml config

```

## Installing and Managing Extensions Through the Lifecycle

ODS manages extensions through a five-phase lifecycle controlled by the Dashboard API ([`routers/extensions.py`](https://github.com/Osmantic/ODS/blob/main/routers/extensions.py)) and security audited by [`scripts/audit-extensions.py`](https://github.com/Osmantic/ODS/blob/main/scripts/audit-extensions.py).

### Discovery and Loading

During the **discovery** phase, [`scripts/resolve-compose-stack.sh`](https://github.com/Osmantic/ODS/blob/main/scripts/resolve-compose-stack.sh) scans `extensions/services/` for enabled [`compose.yaml`](https://github.com/Osmantic/ODS/blob/main/compose.yaml) files (and GPU-specific overlays like [`compose.amd.yaml`](https://github.com/Osmantic/ODS/blob/main/compose.amd.yaml)). Simultaneously, the Dashboard API executes `config.py:load_extension_manifests()` to build the in-memory catalog served at `GET /api/extensions/catalog`.

### Installation and Security Auditing

To install a bundled extension from the library into your personal `$ODS_DATA_DIR/user-extensions/` directory, first run the security audit:

```bash
python3 scripts/audit-extensions.py --project-dir .

```

Then install via the Dashboard API:

```bash
curl -X POST http://localhost:7710/api/extensions/bark/install \
  -H "Authorization: Bearer <API_KEY>"

```

The audit script validates manifests against the schema, checks compose file syntax, and verifies GPU overlay consistency before allowing installation.

### Enabling and Disabling Services

Extensions are toggled by renaming the compose file and invoking the host agent. When you **enable** an extension, the system renames `compose.yaml.disabled` to [`compose.yaml`](https://github.com/Osmantic/ODS/blob/main/compose.yaml) and triggers the host agent to start the container:

```bash
ods enable my-service
ods start my-service

```

To **disable**, the process reverses: the host agent stops the container, then the file is renamed back to `compose.yaml.disabled`:

```bash
ods disable my-service

```

### Uninstalling Extensions

You can only remove disabled extensions. The uninstall command deletes the extension directory from `user-extensions/`:

```bash
ods uninstall my-service

```

## Key Files and Their Roles

Understanding these core files helps you debug and extend ODS effectively:

- **[`scripts/resolve-compose-stack.sh`](https://github.com/Osmantic/ODS/blob/main/scripts/resolve-compose-stack.sh)**: Merges base compose files with enabled extension fragments to generate the final stack configuration.
- **[`scripts/audit-extensions.py`](https://github.com/Osmantic/ODS/blob/main/scripts/audit-extensions.py)**: Validates manifest schemas, compose syntax, and GPU compatibility before installation.
- **[`bin/ods-host-agent.py`](https://github.com/Osmantic/ODS/blob/main/bin/ods-host-agent.py)**: The host agent that executes Docker Compose up/down commands for extension containers.
- **[`extensions/templates/service-template.yaml`](https://github.com/Osmantic/ODS/blob/main/extensions/templates/service-template.yaml)**: Starter template for new extension manifests.
- **[`extensions/templates/compose-template.yaml`](https://github.com/Osmantic/ODS/blob/main/extensions/templates/compose-template.yaml)**: Starter template for Docker Compose fragments.
- **[`docker-compose.base.yml`](https://github.com/Osmantic/ODS/blob/main/docker-compose.base.yml)**: Core infrastructure including `llama-server`, `open-webui`, and the Dashboard API.
- **[`docker-compose.amd.yml`](https://github.com/Osmantic/ODS/blob/main/docker-compose.amd.yml)** / **[`docker-compose.nvidia.yml`](https://github.com/Osmantic/ODS/blob/main/docker-compose.nvidia.yml)**: GPU-specific overlay files applied by the resolver when hardware acceleration is detected.

## Summary

- ODS extensions consist of a [`manifest.yaml`](https://github.com/Osmantic/ODS/blob/main/manifest.yaml) for metadata and a [`compose.yaml`](https://github.com/Osmantic/ODS/blob/main/compose.yaml) for container configuration, stored in `extensions/services/`.
- The **compose stack resolver** ([`scripts/resolve-compose-stack.sh`](https://github.com/Osmantic/ODS/blob/main/scripts/resolve-compose-stack.sh)) dynamically merges enabled extensions with the base stack at runtime.
- The **Dashboard API** loads manifests via `config.py:load_extension_manifests()` and serves them through the catalog endpoint.
- Always validate extensions using [`scripts/audit-extensions.py`](https://github.com/Osmantic/ODS/blob/main/scripts/audit-extensions.py) and the Docker Compose config command before enabling.
- Extensions are installed to `$ODS_DATA_DIR/user-extensions/` and toggled by renaming [`compose.yaml`](https://github.com/Osmantic/ODS/blob/main/compose.yaml) files, with the **host agent** managing container states.

## Frequently Asked Questions

### What is the minimum file structure required for an ODS extension?

Every ODS extension requires exactly two files in its directory: a [`manifest.yaml`](https://github.com/Osmantic/ODS/blob/main/manifest.yaml) containing service metadata (ID, port, health checks, GPU compatibility) and a [`compose.yaml`](https://github.com/Osmantic/ODS/blob/main/compose.yaml) (or GPU-specific variant) defining the Docker container configuration. The directory must reside under `extensions/services/` for system-wide extensions or `$ODS_DATA_DIR/user-extensions/` for user-installed ones.

### How does ODS handle GPU compatibility for extensions?

The manifest `gpu_backends` field specifies which hardware acceleration types an extension supports (e.g., `amd` or `nvidia`). During the discovery phase, [`scripts/resolve-compose-stack.sh`](https://github.com/Osmantic/ODS/blob/main/scripts/resolve-compose-stack.sh) detects the host GPU and automatically includes the appropriate overlay file (such as [`compose.amd.yaml`](https://github.com/Osmantic/ODS/blob/main/compose.amd.yaml)) alongside the base [`compose.yaml`](https://github.com/Osmantic/ODS/blob/main/compose.yaml) when building the final stack.

### Can I install extensions without using the Dashboard API?

Yes, you can manually install extensions by copying them to the `$ODS_DATA_DIR/user-extensions/` directory and running `scripts/audit-extensions.py --project-dir .` to validate them. However, you must still use the `ods enable` and `ods start` commands (or manually rename `compose.yaml.disabled` to [`compose.yaml`](https://github.com/Osmantic/ODS/blob/main/compose.yaml) and invoke [`bin/ods-host-agent.py`](https://github.com/Osmantic/ODS/blob/main/bin/ods-host-agent.py)) to activate the service, as the API handles the lifecycle state transitions and security auditing.

### What happens if an extension fails its health check?

If an extension defines a health check in its [`manifest.yaml`](https://github.com/Osmantic/ODS/blob/main/manifest.yaml) and the container fails to respond at the specified endpoint, the host agent ([`bin/ods-host-agent.py`](https://github.com/Osmantic/ODS/blob/main/bin/ods-host-agent.py)) will report the unhealthy status to the Dashboard API. The extension remains in its current state but is marked unhealthy in the catalog, and dependent services configured with `depends_on` conditions will wait until the health check passes before starting.