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:
- Add to existing opendso-docker-compose - For services that will be used across multiple deployments
- 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.ymlto 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:
-
Keep separate branches:
git checkout -b client-customizations -
Document changes in a CHANGELOG.md
-
Sync with upstream regularly:
git fetch upstream
git rebase upstream/main -
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:
-
Connects to NATS server:
// Node.js example
const nats = require('nats');
const nc = await nats.connect({
servers: process.env.NATS_SERVER || 'nats://nats:4222'
}); -
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}`);
} -
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
- Local Testing: See Local Test Deployment for testing custom services
- Production: See Production Deployment for deploying custom services to production
- Architecture: Review OpenDSO Architecture to understand service integration
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