Docker Deployment
EXD-API plugins are typically deployed as Docker images. This page shows real-world Dockerfiles from production plugins.
ExdFileInterface Plugin (nptdms)
From asam_ods_exd_api_nptdms:
# docker build -t ghcr.io/totonga/asam-ods-exd-api-nptdms:latest .
# docker run --rm -it -v "$(pwd)/data":"$(pwd)/data" -p 50051:50051 ghcr.io/totonga/asam-ods-exd-api-nptdms:latest
FROM python:3.12-slim
LABEL org.opencontainers.image.source=https://github.com/totonga/asam_ods_exd_api_nptdms
LABEL org.opencontainers.image.description="ASAM ODS External Data API for NI TDMS files (*.tdms)"
LABEL org.opencontainers.image.licenses=MIT
WORKDIR /app
# Create a non-root user and change ownership of /app
RUN useradd -ms /bin/bash appuser && chown -R appuser /app
# Copy source code first (needed for pip install)
COPY pyproject.toml .
# Install required packages
RUN pip3 install --upgrade pip && pip3 install .
COPY external_data_file.py ./
USER appuser
# Start server
CMD [ "python3", "external_data_file.py"]
FileSimpleInterface Plugin (pandascsv)
From asam_ods_exd_api_pandascsv:
# docker build -t ghcr.io/totonga/asam-ods-exd-api-pandascsv:latest .
# docker run --rm -it -v "$(pwd)/data":"$(pwd)/data" -p 50051:50051 ghcr.io/totonga/asam-ods-exd-api-pandascsv:latest
FROM python:3.12-slim
LABEL org.opencontainers.image.source=https://github.com/totonga/asam_ods_exd_api_pandascsv
LABEL org.opencontainers.image.description="ASAM ODS External Data API for CSV files (*.csv)"
LABEL org.opencontainers.image.licenses=MIT
WORKDIR /app
# Create a non-root user and change ownership of /app
RUN useradd -ms /bin/bash appuser && chown -R appuser /app
# Copy source code first (needed for pip install)
COPY pyproject.toml .
# Install required packages
RUN pip3 install --upgrade pip && pip3 install .
COPY external_file_data.py ./
USER appuser
# Start server
CMD [ "python3", "external_file_data.py"]
Dockerfile Structure
Both plugins follow the same pattern:
| Step | Purpose |
|---|---|
FROM python:3.12-slim | Minimal Python base image |
LABEL org.opencontainers.image.* | OCI metadata for container registries (source, description, license) |
WORKDIR /app | Set working directory |
RUN useradd ... | Create a non-root user for security |
COPY pyproject.toml | Copy project definition (declares ods-exd-api-box as a PyPI dependency) |
RUN pip3 install . | Install the plugin and all dependencies from PyPI |
COPY external_data_file.py | Copy the plugin entry point script |
USER appuser | Switch to non-root user |
CMD [...] | Start the gRPC server |
The plugin’s pyproject.toml declares ods-exd-api-box (or ods-exd-api-box[simple]) as a dependency. pip install . pulls everything from PyPI — the library source is not copied into the image.
The build/run commands at the top of each Dockerfile use ghcr.io/<owner>/<plugin-name>:latest as the image name, following the convention for GitHub Container Registry.
Building and Running
# Build the image
docker build -t ghcr.io/myorg/my-exd-plugin:latest .
# Run (expose gRPC port, mount data directory)
docker run --rm -it \
-v "$(pwd)/data":"$(pwd)/data" \
-p 50051:50051 \
ghcr.io/myorg/my-exd-plugin:latest
# Run with custom options
docker run --rm -it \
-p 8080:8080 \
ghcr.io/myorg/my-exd-plugin:latest \
python3 external_data_file.py --port 8080 --verbose
The data volume mount -v "$(pwd)/data":"$(pwd)/data" maps the host path identically into the container. This is important because the ODS server passes file paths as URLs (e.g., file:///home/user/data/measurement.tdms) and these paths must resolve identically inside the container.
Mounting Data Files
The ODS server references files using URL paths that must resolve inside the container. The convention is to mirror the host path inside the container:
docker run --rm -it \
-v "$(pwd)/data":"$(pwd)/data" \
-p 50051:50051 \
ghcr.io/myorg/my-exd-plugin:latest
Alternatively, mount to a fixed path:
docker run --rm -it \
-p 50051:50051 \
-v /path/to/data:/data:ro \
ghcr.io/myorg/my-exd-plugin:latest
TLS with Docker
Mount your certificates as a volume:
docker run --rm -it \
-p 50051:50051 \
-v /path/to/certs:/certs:ro \
-v "$(pwd)/data":"$(pwd)/data" \
ghcr.io/myorg/my-exd-plugin:latest \
python3 external_data_file.py \
--use-tls \
--tls-cert-file /certs/server.crt \
--tls-key-file /certs/server.key
Mutual TLS:
docker run --rm -it \
-p 50051:50051 \
-v /path/to/certs:/certs:ro \
-v "$(pwd)/data":"$(pwd)/data" \
ghcr.io/myorg/my-exd-plugin:latest \
python3 external_data_file.py \
--use-tls \
--tls-cert-file /certs/server.crt \
--tls-key-file /certs/server.key \
--tls-client-ca-file /certs/client-ca.crt \
--require-client-cert
Using Environment Variables
Environment variables are often more convenient in container orchestration:
docker run --rm -it \
-p 50051:50051 \
-v "$(pwd)/data":"$(pwd)/data" \
-e ODS_EXD_API_PORT=50051 \
-e ODS_EXD_API_VERBOSE=true \
-e ODS_EXD_API_AUTO_CLOSE_INTERVAL=60 \
-e ODS_EXD_API_AUTO_CLOSE_IDLE=300 \
ghcr.io/myorg/my-exd-plugin:latest
Health Checks
Enable the health check service for container orchestrators:
docker run --rm -it \
-p 50051:50051 \
-p 50052:50052 \
-v "$(pwd)/data":"$(pwd)/data" \
-e ODS_EXD_API_HEALTH_CHECK_ENABLED=true \
ghcr.io/myorg/my-exd-plugin:latest
Docker health check directive
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python3 -c "import grpc; ch=grpc.insecure_channel('localhost:50052'); grpc.channel_ready_future(ch).result(timeout=3)" || exit 1
Docker Compose
services:
exd-plugin:
image: ghcr.io/myorg/my-exd-plugin:latest
# or build from source:
# build: .
ports:
- "50051:50051"
- "50052:50052"
environment:
ODS_EXD_API_HEALTH_CHECK_ENABLED: "true"
ODS_EXD_API_VERBOSE: "true"
volumes:
- ./data:./data
healthcheck:
test: ["CMD", "python3", "-c",
"import grpc; ch=grpc.insecure_channel('localhost:50052'); grpc.channel_ready_future(ch).result(timeout=3)"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
Production Tips
- Non-root user — Always run as a non-root user (the example Dockerfile does this).
- Read-only volumes — Mount data and certificate volumes as
:ro. .dockerignore— Excludetests/,.venv/,*.egg-info/,.git/to keep images small.- Multi-stage builds — For large dependencies, use a build stage to install packages and copy only the runtime into the final image.
- Pinning — Pin your base image and dependency versions for reproducible builds.