This tutorial describes how to develop and test an external integration without rebuilding a
Docker image for each change: your code runs as a simple Node.js process on your machine and connects to a Gladys instance running locally.
This is the fastest development loop, and it complements step 4 of the official documentation.
What you will do
An external integration authenticates with Gladys using a token (a JWT) and a
selector (its unique identifier). These two values are created by Gladys when it
installs the integration (in developer mode) and builds its Docker container.
The trick of local development is therefore to:
- install the integration once in Gladys, which generates the token
- retrieve this token and delete the container
- run your code locally with this token — Gladys doesn’t see the difference.
┌──────────────────────────┐ WebSocket + REST ┌─────────────────────────┐
│ Gladys (npm start) │ ◄────────────────────────────────► │ your integration │
│ API localhost:1443 │ token + selector │ node index.js │
│ Front localhost:1444 │ │ (outside container) │
└──────────────────────────┘ └─────────────────────────┘
The Docker container is only used to obtain the token: the code you test runs alongside it.
Prerequisites
Gladys running locally
From the root of the Gladys repository:
nvm use 22
npm start
This command launches in parallel the server (API on http://localhost:1443) and the front (http://localhost:1444). Open the front and create your administrator account if this is a first installation.
Docker installed and started
The daemon must respond to the following command without error:
docker ps
Non-standard Docker socket
If you are using Colima (or a non-standard Docker socket)
Gladys communicates with Docker via the dockerode library, which does not read Docker contexts. It
only knows two things: the environment variable DOCKER_HOST, and by default the socket
/var/run/docker.sock.
With Colima, Podman, Rancher Desktop, or a remote Docker, this socket does not exist:
ls -la /var/run/docker.sock # No such file or directory
Your docker CLI still works (it uses the context), but Gladys, on the other hand, will
see no daemon and external integrations will be disabled.
Export the variable in the shell that launches Gladys, before npm start:
export DOCKER_HOST=$(docker context inspect --format '{{.Endpoints.docker.Host}}')
# ex. unix:///Users/me/.colima/default/docker.sock
npm start
Permanent alternative, if you prefer not to think about it anymore:
sudo ln -sf ~/.colima/default/docker.sock /var/run/docker.sock
Symptom if the step is forgotten: at startup, the server logs
External integrations are not available: Gladys has no access to a Docker socket, and any attempt to install an external integration fails.
Step 1 — Install the integration in developer mode
Go to http://localhost:1444/dashboard/integration (the button only appears for an administrator account), then click on « Install from GitHub ».
In the window that opens, unfold the link « Developer mode: install from a
Docker image ».
Two fields are offered to you:
| Field | Required | Remark |
|---|---|---|
| Docker Image | Yes | Takes precedence over the docker_image declared in the manifest |
| Manifest (JSON, optional) | No | Unnecessary if the image has the label io.gladysassistant.manifest, otherwise paste the contents of your gladys-assistant-integration.json |
Click on Install.
What image to provide?
a) An image published on Github — the nominal case
ghcr.io/<owner>/<repo>:<version>
This is what you have if you have a GitHub repository. The CI already publishes images. The content of the image is not important here, since you will anyway run your code locally.
b) A « placeholder » image + the manifest pasted — to start without publishing anything
Haven’t published anything yet? Use any lightweight public image and paste your
manifest in the second field:
alpine:3
Gladys downloads alpine, validates your manifest gladys-assistant-integration.json, creates the service and generates the token: that’s all you need. The container won’t do anything useful (it will stop immediately, the status will go to « Degraded » then « Error ») — of no importance, it will be deleted in step 3.
c) A locally built image (pending PR #2841)
Build a local image from the development directory of your external integration
docker build -t <my-integration>:dev .
Variant: Install from the GitHub repository URL
The main form of the same window accepts a repository URL:
https://github.com/<owner>/<repo>
Gladys reads the gladys-assistant-integration.json at the root and downloads the image declared in the manifest. Practical when the repository is public and the image published — without manifest to copy.
Only difference for the rest: the selector becomes ext-<owner>-<repo> instead of
ext-dev-<name>.
Do you need to launch it once?
No, it’s automatic. The installation chains: image download → service creation in the database → container creation → start. It is this container creation that creates the token, so it exists as soon as the form ends — you are redirected to the integration page.
If the manifest is invalid, Gladys displays the details field by field (actions[1].depends_on: unknown field): it’s a developer screen, the errors are explicit.
Step 2 — Retrieve the selector and the token
The selector
After installation, you are redirected to the integration page. The selector is the last
segment of the URL:
http://localhost:1444/dashboard/integration/device/external/<my-selector>
Warning, this is not the name of your integration. Gladys derives it:
| Installation mode | Selector |
|---|---|
| Developer mode (Docker image) | ext-dev-<manifest-name-in-lowercase-with-hyphens> |
| From a GitHub repository URL | ext-<owner>-<repo> |
In case of collision, a numerical suffix is added (ext-dev-my-integration-2).
This is not just a label: the SDK uses it to prefix the identifiers of your devices
(ext:<selector>:<my-device>), and Gladys rejects any identifier that falls outside this perimeter.
The token
The token is never displayed in the interface: Gladys injects it into the container’s
environment and does not display it again. We therefore read it directly in the container, which always
has the name gladys-<selector>:
docker inspect gladys-<my-selector> \
--format '{{range .Config.Env}}{{println .}}{{end}}' | grep GLADYS_
Expected output:
GLADYS_HOST_API_URL=http://host.docker.internal:1443
GLADYS_INTEGRATION_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
GLADYS_INTEGRATION_SELECTOR=ext-dev-my-integration
You have your three variables. Two clarifications:
GLADYS_HOST_API_URLishost.docker.internalbecause the value is written for a container. From your machine, it will behttp://localhost:1443.- The token has no expiration date: it remains valid as long as Gladys does not recreate the
container (see the Troubleshooting section). - We find the name of the selector found previously thanks to the URL
GLADYS_INTEGRATION_SELECTOR
Step 3 — Delete the container
A token cannot be shared: when a second client authenticates with the same token,
Gladys closes the first connection. If the container remains alive, it and your local process
cut each other off in a loop.
So delete the container:
docker rm -f gladys-<my-selector>
The token remains valid: its revocation only depends on a counter in the database, which this deletion does not affect.
Step 4 — Launch the integration with Node
In your integration repository:
nvm use 22
npm install
Then launch it with the three retrieved variables:
GLADYS_HOST_API_URL="http://localhost:1443" \
GLADYS_INTEGRATION_TOKEN="<the copied token>" \
GLADYS_INTEGRATION_SELECTOR="<my-selector>" \
LOG_LEVEL=debug \
npm start
Verify that it works
On the integration side, the logs display the connection to the WebSocket. On the Gladys side, refresh the
integration page: the status goes to « Running », even though no container is
running. You can now normally use the Configuration, Discovery,
Devices and Actions tabs.

Loop
Modify your code, Ctrl-C, restart.
The token survives all restarts of your process: no need to regenerate it.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
401 on the API, or WebSocket closure with code 4000 |
The token has been revoked: Gladys recreated the container (integration update, hardware modification, or Gladys server restart) | Go back to step 2 to read the token in the new container, then do step 3 again |
| The container reappears on its own and cuts your connection | Step 3 not done, or done with docker stop / the « Stop » button |
docker rm -f gladys-<my-selector> |
UNABLE_TO_PULL_IMAGE during installation |
The image does not exist on any accessible registry | Use a placeholder image (step 1b), or the branch of PR #2841 |
Installation fails with a generic error, and the server logs External integrations are not available |
Gladys does not see the Docker daemon | Export DOCKER_HOST before npm start (see Prerequisites) |
| « Degraded » then « Error » status just after installation | Expected with a placeholder image: the container never authenticates | No consequence — the token remains valid, continue |
GladysIntegration: missing "…" option |
One of the three variables was not passed | Check the launch command / the source of the .env.local |
The integration connects, but discovery fails with devices[0].external_id: must start with "ext:…:" |
The selector passed does not match that of the service: the SDK prefixes the external_id with ext:<selector>:, and Gladys rejects anything that falls outside its perimeter |
Take the exact selector in the integration page URL (step 2) |
After each restart of the Gladys server, the deleted container is recreated at the start
of the integration, with a new token: read it again (step 2) and delete the
container again (step 3). This is the main surprise of this development loop.
Cleanup
To start from a clean slate, uninstall the integration from its Supervision tab →
Uninstall. Gladys deletes the container, its private network, its data folder and the
corresponding line in the database. Devices already created remain in Gladys until you delete them.
There’s nothing left but to go back to step 1.




