Skip to content

Auxiliary Services

Services Dashboard

Oduflow can manage sidecar containers for auxiliary services your Odoo instance depends on — Redis, Meilisearch, Elasticsearch, RabbitMQ, or any other Docker-based service.

Creating a Service

# Redis
oduflow call create_service redis redis:7 6379

# Meilisearch with environment variables. A value "secret:<name>" references a
# write-only team secret set in the dashboard — see Security → Secrets.
oduflow call create_service meilisearch getmeili/meilisearch:v1.6 7700 "" "MEILI_MASTER_KEY=secret:meili-master-key,MEILI_ENV=production"

# Elasticsearch
oduflow call create_service elasticsearch docker.elastic.co/elasticsearch/elasticsearch:8.11.0 9200 "" "discovery.type=single-node,ES_JAVA_OPTS=-Xms512m -Xmx512m"

# MinIO — the image expects its start arguments as the container command
oduflow call create_service '{"name":"minio","image":"minio/minio","port":9000,"command":"server /data"}'

# WireGuard VPN — needs NET_ADMIN to manage tun/iptables
oduflow call create_service '{"name":"vpn","image":"linuxserver/wireguard","port":51820,"net_admin":true}'

# Publish only selected HTTP prefixes, each on its own backend port
oduflow call create_service '{
  "name":"fs",
  "image":"oduist/freeswitch:latest",
  "hostname":"fs",
  "host_mode":true,
  "routes":[
    {"path":"/RPC2","port":8080,"strip_prefix":false},
    {"path":"/portal","port":8080,"strip_prefix":false}
  ]
}'

Services are:

  • Attached to the team's isolated Docker network oduflow-{team_id}-net (reachable by that team's Odoo containers and other services)
  • Given an unless-stopped restart policy
  • Automatically routed through Traefik with HTTPS when in traefik mode
  • In Traefik TLS mode, automatically given the exact system mount oduflow-traefik-acme:/etc/traefik:ro
  • Labeled for management (oduflow.managed=true, oduflow.service=<name>)
  • Always created from a freshly pulled image — create_service and restore_service explicitly pull before running, so mutable tags like :latest get the current published version instead of a stale local cache

Each team has service_slots = 10 by default. The cap includes running and stopped managed services; updating or restarting an existing service does not consume another slot. Delete an unused service to free capacity, or set service_slots = 0 to disable the cap.

Connecting from Odoo

Inside the team's Docker network the service is reachable by its container name — the DNS name is exactly oduflow-{team_id}-svc-{name} (e.g. oduflow-1-svc-redis). There is no shorter alias such as redis or oduflow-svc-redis. This is precisely the Container: / Internal hostname: value that create_service and get_service_info report, so configure Odoo against that:

oduflow-1-svc-redis:6379

The URL: line printed by the service tools is the external Traefik/host address, not the internal one — do not use it for in-cluster connections.

host_mode services are not on the team network, so they are not resolvable by container name; reach them via host.docker.internal instead.

Restricted HTTP Path Routes

In Traefik mode, routes can replace the single catch-all port. Each route publishes a URL prefix on the service's hostname and forwards it to another HTTP port of the same service. This works in both networking modes:

  • Bridge services are reached on their private IP in the team's Docker network.
  • host_mode services are reached through host.docker.internal.

Routes are prefix matches on path-segment boundaries: /api accepts /api and /api/..., but not /apix. When routes are present Oduflow does not create the hostname-only catch-all router, so every unlisted path receives Traefik's 404 without reaching the service. Set strip_prefix=true when the backend serves from /; Traefik then sends /portal/assets/app.js as /assets/app.js and adds X-Forwarded-Prefix: /portal.

routes is intentionally not an arbitrary reverse-proxy configuration: a route contains only path, port, and optional strip_prefix, and always targets the same managed service. It is available only with [routing].mode = "traefik". Raw TCP/UDP protocols cannot be routed by URL path.

Traefik Certificate Store

When Oduflow terminates TLS through Traefik, every auxiliary service can read Traefik's certificate store at /etc/traefik/acme.json. The mount is implicit: do not add oduflow-traefik-acme to the volumes argument, and do not mount a different volume at /etc/traefik. Oduflow always mounts the exact system volume read-only; there is no wildcard allowance for other oduflow-* volumes. The implicit mount is runtime configuration and is not saved in the service preset.

This deliberately makes the shared certificate and private-key material readable to every service container. Use only trusted service images and grant service-management access only to trusted operators. Read-only protects the store from modification, not from disclosure.

Start Command

Some images ship an ENTRYPOINT that expects arguments, and their default CMD is not what you want (minio/minio needs server /data, for example). The optional command replaces the image CMD:

oduflow call create_service '{
  "name":"minio",
  "image":"minio/minio",
  "port":9000,
  "command":"server /data --console-address :9001"
}'

The string is split with shell quoting rules, so --address ":9001" stays one argument; the resulting argv is what get_service_info reports and what the preset stores. The image ENTRYPOINT is never changed. Leave command out to run the image's own CMD.

On update_service the parameter is tri-state: omitted keeps the current command, a new string replaces it, and an empty string drops the override so the container falls back to the image CMD. (This differs from env_vars and image, where an empty value means "keep".)

# Change the command
oduflow call update_service '{
  "name":"minio",
  "command":"server /data --console-address :9090"
}'

# Drop it and use the image default again
oduflow call update_service '{"name":"minio","command":""}'

Linux Capabilities

Two optional flags grant additional container privileges:

  • net_admin — adds the NET_ADMIN Linux capability. Required for VPN / WireGuard, tun/tap devices, and iptables manipulation inside the container.
  • privileged — runs the container in privileged mode (full host access, all capabilities). Use with care — only when a service genuinely needs it (e.g. Docker-in-Docker, hardware passthrough).

Both can be enabled at the same time; on the Docker side, privileged implies all capabilities so net_admin is then redundant — but it is still recorded in the preset so disabling privileged later keeps NET_ADMIN active.

Managing Services

# List all services with status, ports, URLs, and env vars
oduflow call list_services

# Full state of a single service — image + digest, port/routes, hostname,
# host_mode, command, volumes, env vars, capabilities, restart count,
# started_at, preset
oduflow call get_service_info redis

# View service logs
oduflow call get_service_logs redis 200

# Restart a service
oduflow call restart_service redis

# Update a service (pull latest image, recreate container with same settings)
oduflow call update_service meilisearch

# Change environment variables on a running service (fully replaces existing env_vars)
oduflow call update_service '{"name":"meilisearch","env_vars":"MEILI_MASTER_KEY=newkey,MEILI_ENV=production"}'

# Change the image (tag) of a running service
oduflow call update_service '{"name":"meilisearch","image":"getmeili/meilisearch:v1.8"}'

# Toggle Linux capabilities / privileged mode on a running service (recreates it)
oduflow call update_service '{"name":"wireguard","net_admin":true}'
oduflow call update_service '{"name":"wireguard","privileged":true}'

# Delete a service (its preset is kept, so restore_service can bring it back)
oduflow call delete_service redis

# Delete a service and its saved preset, leaving nothing behind
oduflow call delete_service '{"name":"redis","save_preset":false}'

# Execute a command inside a service container
oduflow call run_service_command redis "redis-cli ping"

Changing a Service

update_service is the preferred way to change any setting of a running service — image, env vars, port/routes, hostname, host_mode, command, volumes, privileged, or net_admin. It recreates the container automatically and preserves every setting you do not override, so you rarely need to delete and recreate a service by hand. Passing routes fully replaces the route list. To return to a single catch-all port, pass routes=[] and the replacement port in the same call.

In the dashboard, Update on a service card opens the same editor as the MCP tool: image, command, exposure (a single port or restricted path routes), hostname, environment variables, host network mode, volumes and capabilities, all prefilled with the service's current configuration (from its preset, or from the container for a service created before presets). Confirm to apply the changes and pull the latest image in one go; confirm without editing anything to just pull and recreate. Only the fields you actually edited are sent, so an open dialog cannot revert a setting changed concurrently over MCP. Clearing the env or volumes field removes every variable or unmounts every volume; clearing the command falls back to the image CMD. An empty hostname keeps the current one. Only runtime has no form field — change it over MCP or the CLI.

If you do recreate a service manually (e.g. to rename it), call get_service_info first and reuse its fields in the new create_service call. The returned dict carries the full configuration (image, port or routes, hostname, env_vars, host_mode, command, volumes, cap_add, privileged) so you do not lose anything that list_services truncates or that lived only inside the preset.

Protecting a Service

A service can be protected from the dashboard (the Protect button on its card). While protected, delete_service, update_service, and restore_service are refused — over MCP, the CLI, and the dashboard alike — so an agent cannot recreate or remove a service that backs something important. Restart and logs remain available. Protection can only be toggled in the dashboard; there is no MCP tool for it, so an agent cannot lift it.

Service Update Flow

The update_service operation:

  1. Reads the saved preset (authoritative source) or inspects the running container as a legacy fallback
  2. Applies any overrides passed in (env_vars, image, port/routes, hostname, host_mode, command, volumes, privileged, net_admin) — each override fully replaces the current value
  3. Resolves the complete candidate volume configuration before touching the running container; invalid or missing volumes fail without stopping it
  4. Pulls the target image (the override, or the current one)
  5. Decides whether to recreate:
    • If neither the image digest nor any setting changed → reports "already up-to-date"
    • If the image digest changed, any setting was overridden, or a legacy Traefik TLS service lacks the implicit ACME mount → stops the old container, removes it, and creates a new one with the merged settings
  6. Updates the saved preset so subsequent restore_service calls use the new configuration

Overrides are optional: calling update_service with only name pulls the current image and recreates only when its digest changed or the implicit ACME mount is missing.

Service Presets

Every time a service is created or updated, its configuration (image, port or routes, hostname, environment variables, volumes, host_mode, command, cap_add, privileged) is automatically saved as a preset in {team_data_dir}/service_presets.json. This allows you to restore a service after deletion without re-entering its configuration.

Deleting a service keeps its preset by default — delete_service takes save_preset (the dashboard's delete dialog has a Save as preset checkbox, ticked by default). Pass save_preset=false to drop the preset together with the container. Services created before presets existed get theirs backfilled once at server start (migration 0008-backfill-service-presets), so old and new services read from the same store; the delete result reports whether a preset actually remains on disk.

# List saved presets
oduflow call list_service_presets

# Restore a previously deleted service
oduflow call restore_service redis

# Remove a saved preset
oduflow call delete_service_preset redis

Container lifecycle settings

MCP/REST create_service and update_service accept a runtime mapping. It is also available on Stack services. Supported keys are tmpfs (only /run, /run/lock, /tmp), cgroupns: private, stop_signal (SIGTERM or SIGRTMIN+3) and stop_timeout (1–3600 seconds). For example:

{
  "tmpfs": {"/run": "rw,nosuid,nodev,mode=755", "/tmp": "rw,nosuid,nodev,mode=1777"},
  "cgroupns": "private",
  "stop_signal": "SIGRTMIN+3",
  "stop_timeout": 240
}

In Stack manifests the keys follow the manifest-wide camelCase convention (stopSignal, stopTimeout); the snake_case spellings shown above are also accepted there.

Updates preserve these settings when omitted; {} clears the explicit overrides. Presets and stack planning retain them. Stop/restart/replacement honors the old container's timeout so changing an image does not truncate its shutdown grace period. These settings do not grant privileges or make a systemd image healthy: an image must implement its own readiness check, and its cgroup/capability requirements must be verified on the deployment host. The runtime field does not accept arbitrary Docker options or bind mounts from the host.