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

ODS supports customization through self-contained service extensions that combine a 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): Defines service metadata including the unique ID, exposed ports, health check endpoints, GPU compatibility flags, and category classification.
  • A Docker Compose fragment (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) aggregates the base infrastructure (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), 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:

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

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

Finally, define the compose.yaml fragment to configure the container. This example demonstrates environment variable injection, service dependencies, and health check configuration:

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:

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:

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) and security audited by scripts/audit-extensions.py.

Discovery and Loading

During the discovery phase, scripts/resolve-compose-stack.sh scans extensions/services/ for enabled compose.yaml files (and GPU-specific overlays like 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:

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

Then install via the Dashboard API:

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 and triggers the host agent to start the container:

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:

ods disable my-service

Uninstalling Extensions

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

ods uninstall my-service

Key Files and Their Roles

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

Summary

  • ODS extensions consist of a manifest.yaml for metadata and a compose.yaml for container configuration, stored in extensions/services/.
  • The compose stack resolver (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 and the Docker Compose config command before enabling.
  • Extensions are installed to $ODS_DATA_DIR/user-extensions/ and toggled by renaming 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 containing service metadata (ID, port, health checks, GPU compatibility) and a 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 detects the host GPU and automatically includes the appropriate overlay file (such as compose.amd.yaml) alongside the base 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 and invoke 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 and the container fails to respond at the specified endpoint, the host agent (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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →