1
Custom Services
anti edited this page 2026-04-18 06:04:39 -04:00

Custom Services (Bring-Your-Own)

DECNET ships 25+ first-class service plugins (see Services catalog), but you can add your own without writing a plugin by declaring a [custom-<name>] section in the INI.

The source of truth is decnet/custom_service.py (the CustomService class) and the [custom-*] branch of decnet/ini_loader.py.


How it works

At INI load time the parser scans every section starting with custom-:

# decnet/ini_loader.py
if section.startswith("custom-"):
    s = cp[section]
    svc_name  = section[len("custom-"):]
    image     = s.get("binary", "")
    exec_cmd  = s.get("exec", "")
    ports_raw = s.get("ports", "")
    ports = [int(p.strip()) for p in ports_raw.split(",") if p.strip().isdigit()]
    cfg.custom_services.append(
        CustomServiceSpec(name=svc_name, image=image, exec_cmd=exec_cmd, ports=ports)
    )

Each resulting CustomServiceSpec is wrapped into a CustomService (subclass of BaseService) and registered dynamically at deploy time (register_custom_service() in the service registry). Once registered, the custom slug behaves like any built-in service — you can list it in a decky's services= line and reference it in the --services CLI flag.

CustomService.compose_fragment() produces a Docker Compose fragment of the shape:

image: <binary>
container_name: <decky>-<slug>
restart: unless-stopped
environment:
  NODE_NAME: <decky>
  LOG_TARGET: <optional, from --log-target>
command: [<tokens from exec=>]   # only emitted if exec= is non-empty

Underscores in the custom name are converted to dashes in the container suffix (slug = name.replace("_", "-")).

Note: CustomService.dockerfile_context() returns None, so DECNET will not try to build the image. The image you name in binary= must be pullable (public registry, or present in the local Docker daemon).


Accepted keys

Key Required Meaning
binary yes Docker image reference, e.g. myorg/weirdapp:1.2.3. Becomes the container's image:.
exec no Command to run inside the container. Space-split into a list and emitted as Compose command:. If omitted, the image's default CMD/ENTRYPOINT is used.
ports no Comma-separated list of integer ports. Non-numeric tokens are silently dropped. Used by the deploy layer to expose/route the ports.

There are no other keys. Anything else in the section is ignored by ini_loader.py.

The custom service slug (the part after custom-) must not collide with a built-in service name.


Minimal working example

Ship a lightweight HTTP echo server as a decoy on port 9000:

[general]
net       = 192.168.1.0/24
gw        = 192.168.1.1
interface = eth0

# Define the custom service
[custom-echoweb]
binary = ealen/echo-server:0.9.2
exec   = node index.js
ports  = 80

# Use it on a decky, alongside a built-in service
[decky-app01]
ip       = 192.168.1.130
services = ssh, echoweb
nmap_os  = linux

[decky-app01.ssh]
ssh_version    = OpenSSH_8.9p1 Ubuntu-3ubuntu0.6
kernel_version = 5.15.0-91-generic
users          = root:toor

Deploy:

sudo decnet deploy --config echo.ini --interface eth0 \
     --log-target 192.168.1.200:5140

At deploy time, decky-app01 will get two containers: a standard ssh decoy and a decky-app01-echoweb container running ealen/echo-server:0.9.2 with command: ["node", "index.js"].


Caveats

  • No per-service persona subsection is honoured for custom services — the service_cfg argument in CustomService.compose_fragment() is accepted but ignored. If you need persona/override behaviour, write a real plugin.
  • No Dockerfile build step. binary= is pulled as-is.
  • If exec= is empty, no command: key is emitted — the image's default entrypoint runs.
  • LOG_TARGET is injected into the container env only if a CLI --log-target was passed to decnet deploy; otherwise the custom service is responsible for its own logging.

See also: