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