Skip to main content

Adding Custom Services to OpenDSO Deployment

This guide explains how to extend an OpenDSO deployment with custom applications and services.

Overview

OpenDSO deployments can be extended with custom applications in two ways:

  1. Add to existing opendso-docker-compose - For services that will be used across multiple deployments
  2. Fork repositories - For client-specific customizations that require many custom applications

Understanding the Repository Structure

Recall that OpenDSO deployments consist of three repositories:

deployment/
├── opendso/ # Generic OpenDSO orchestration
├── config/ # Client-specific configuration
└── models/ # OpenFMB adapter configurations

Custom services can be added by:

  • Modifying opendso/docker-compose.yml to add service definitions
  • Adding configuration in config/docker/.env
  • Creating necessary config files in config/

Adding Services to OpenDSO

Step 1: Create Dockerfile

Create a Dockerfile for your application if one doesn't exist. The structure depends on your application type (Node.js, Python, Rust, etc.).

Step 2: Build and Push Docker Image

Build the image and push to your Docker registry:

# Navigate to application directory
cd your-service

# Build image
docker build -t yourorg/your-service:latest .

# Tag with version
docker tag yourorg/your-service:latest yourorg/your-service:1.0.0

# Login to Docker registry
docker login

# Push images
docker push yourorg/your-service:latest
docker push yourorg/your-service:1.0.0

Step 3: Add Service to docker-compose.yml

Add the service definition to opendso/docker-compose.yml.

Example: Custom Backend Service

services:
# ... existing services ...

custom-service:
image: yourorg/custom-service:${CUSTOM_SERVICE_TAG}
container_name: custom-service
networks:
- opendso
environment:
- NATS_SERVER=nats://nats-main:4222
- MONGODB_URI=mongodb://mongodb:27017
- LOG_LEVEL=info
volumes:
- ../config/custom-service:/app/config:ro
- ../output/custom-service:/app/logs
depends_on:
- nats-main
- mongodb
restart: unless-stopped
profiles:
- services
- all

Key Configuration Elements:

  • image: Docker image with tag from environment variable
  • networks: Must be on the OpenDSO network for NATS communication
  • environment: Runtime configuration (NATS server, database URIs, etc.)
  • volumes: Mount config files (read-only) and log directories
  • depends_on: Service dependencies
  • profiles: Which deployment profiles include this service
  • No port mapping unless external access is needed

Step 4: Add Configuration Variables

Add the necessary environment variables to config/docker/.env:

############################
# Custom Services:
############################
CUSTOM_SERVICE_TAG="1.0.0"

Step 5: Test the Deployment

Deploy and verify the new service:

cd opendso-docker-compose

# Deploy with services profile
./run.sh -p services -c

# Verify container is running
docker ps | grep custom-service

# Check logs
docker-compose logs -f custom-service

Forking Repositories for Heavy Customization

When a deployment requires many custom applications, fork the repositories instead of modifying the original.

When to Fork

Fork repositories when:

  • Making structural changes to deployment
  • Client-specific modifications that shouldn't affect other deployments
  • Need independent version control for client deployment

Fork Process

# Fork opendso-docker-compose on GitHub
# Then clone your fork

git clone https://github.com/your-org/opendso-docker-compose.git
cd opendso-docker-compose

# Add upstream remote to pull updates
git remote add upstream https://github.com/openenergysolutions/opendso-docker-compose.git

# Pull updates from upstream when needed
git fetch upstream
git merge upstream/main

Managing Forked Repositories

Best Practices:

  1. Keep separate branches:

    git checkout -b client-customizations
  2. Document changes in a CHANGELOG.md

  3. Sync with upstream regularly:

    git fetch upstream
    git rebase upstream/main
  4. Tag releases:

    git tag -a v1.0.0-client -m "Client deployment v1.0.0"
    git push origin v1.0.0-client

Common Service Patterns

Pattern 1: Static Web Application (Vue, React, Angular)

FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Pattern 2: Node.js API Service

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Pattern 3: Python Service

FROM python:3.11-alpine
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]

Pattern 4: Rust/C++ Service

FROM rust:1.70 AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release

FROM debian:bullseye-slim
RUN apt-get update && apt-get install -y libssl1.1 ca-certificates
COPY --from=builder /app/target/release/service /usr/local/bin/service
CMD ["service"]

Configuration Management

Environment Variables

Add service-specific variables to config/docker/.env:

# EDO-ADR Configuration
EDO_ADR_TAG="1.0.0"
EDO_ADR_PORT="8080"
EDO_ADR_NATS_SERVER="nats://nats:4222"

# Custom API Configuration
CUSTOM_API_TAG="2.1.3"
CUSTOM_API_LOG_LEVEL="info"
CUSTOM_API_DB_HOST="mongodb"

Configuration Files

Mount configuration files from config/ directory:

  custom-service:
# ... other config ...
volumes:
- ../config/custom-service/app-config.json:/app/config.json:ro
- ../config/custom-service/certs:/app/certs:ro

Create the config directory structure:

mkdir -p config/custom-service
cat > config/custom-service/app-config.json <<EOF
{
"natsServer": "nats://nats:4222",
"logLevel": "info",
"features": {
"enableMetrics": true
}
}
EOF

Integration with OpenDSO Services

Connecting to NATS Message Bus

Most OpenDSO services communicate via NATS. Ensure your service:

  1. Connects to NATS server:

    // Node.js example
    const nats = require('nats');
    const nc = await nats.connect({
    servers: process.env.NATS_SERVER || 'nats://nats:4222'
    });
  2. Subscribes to relevant topics:

    // Subscribe to OpenFMB topics
    const sub = nc.subscribe('openfmb.loadmodule.LoadControlProfile.*');
    for await (const msg of sub) {
    console.log(`Received: ${msg.subject}`);
    }
  3. Publishes events:

    // Publish OpenFMB protobuf message
    nc.publish('openfmb.loadmodule.LoadControlProfile.device123', messageBytes);

Connecting to MongoDB (GMS API Database)

  custom-service:
environment:
- MONGODB_URI=mongodb://admin:password@mongodb:27017/settings_api?authSource=admin
depends_on:
- mongodb

Connecting to Other Services

Use Docker service names for internal communication:

  custom-ui:
environment:
- API_URL=http://api:3000
- HISTORIAN_URL=http://historian:8080
- NATS_SERVER=nats://nats:4222

Testing Custom Services

Local Testing

# Build and test locally
docker build -t myservice:test .
docker run --rm -p 8080:80 myservice:test

# Test with docker-compose
docker compose -f compose.yaml up custom-service

Integration Testing

# Deploy with dependencies
./run.sh -p nats -p api -p custom-service -c

# Check logs
docker compose logs -f custom-service

# Verify connectivity
docker exec custom-service ping nats
docker exec custom-service curl http://api:3000/health

Troubleshooting Custom Services

For troubleshooting custom service deployment issues, see the Docker Troubleshooting Guide.

Common issues covered include:

  • Container build failures
  • Container won't start
  • Network communication problems
  • Configuration and environment variable issues
  • Service discovery issues
  • Logging problems

The guide includes specific examples for backend services (like mock-der-dispatch) and frontend services (like edo-adr).

Best Practices

1. Use Multi-Stage Builds

Minimize image size and improve security:

FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM node:18-alpine
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/server.js"]

2. Health Checks

Add health checks to service definitions:

  custom-service:
# ... other config ...
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

3. Resource Limits

Prevent resource exhaustion:

  custom-service:
# ... other config ...
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M

4. Logging

Use consistent logging formats and levels:

  custom-service:
# ... other config ...
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

5. Version Pinning

Always use specific version tags, never latest:

# Good
EDO_ADR_TAG="1.0.0"

# Bad
EDO_ADR_TAG="latest"

Next Steps

Support

For questions about adding custom services:

  • Review existing services in opendso/compose.yaml
  • Contact the OpenDSO software team
  • Consult with your OES Support team