Deploy
Run the agent permanently as a systemd service or a Docker container, and rotate the device token.
On production devices, run the agent under a supervisor (a program that starts a process again when it exits). The agent does not re-execute itself, so after a restart command from the server (exit code 3), a crash or a power cut, the supervisor must start it again. Pick either systemd or Docker.
Either way there are two config files.
| File | Contents | Permissions |
|---|---|---|
/etc/geo-mlops/edge.yaml | Site setup — device name, collectors, retention limits | Readable |
/etc/geo-mlops/edge.env | Secrets — one GEO_EDGE_*=value per line. At least GEO_EDGE_CENTRAL__TOKEN | 600, owner only |
systemd
-
Create a dedicated account and a virtual environment, and install.
sudo useradd --system --home /var/lib/geo-mlops-edge --shell /usr/sbin/nologin geo-mlops sudo python3 -m venv /opt/geo-mlops/venv sudo /opt/geo-mlops/venv/bin/pip install 'geo-mlops-sdk[edge,gpu,modbus]>=0.2,<0.3' -
Put the two config files in place.
sudo mkdir -p /etc/geo-mlops sudo cp edge.yaml /etc/geo-mlops/edge.yaml echo 'GEO_EDGE_CENTRAL__TOKEN=<device-token>' | sudo tee /etc/geo-mlops/edge.env >/dev/null sudo chmod 600 /etc/geo-mlops/edge.env -
Save the unit below as
/etc/systemd/system/geo-mlops-edge.serviceand enable it.sudo systemctl daemon-reload sudo systemctl enable --now geo-mlops-edge journalctl -u geo-mlops-edge -f
# /etc/systemd/system/geo-mlops-edge.service
[Unit]
Description=MLOps Central Platform edge agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=geo-mlops
Group=geo-mlops
# Secrets go here. This file is also where you change the token
EnvironmentFile=-/etc/geo-mlops/edge.env
Environment=GEO_EDGE_CONFIG=/etc/geo-mlops/edge.yaml
ExecStart=/opt/geo-mlops/venv/bin/geo-mlops-edge run
WorkingDirectory=/var/lib/geo-mlops-edge
# Restart on 0 (stop requested), 3 (restart requested) and crashes alike
Restart=always
RestartSec=2
# On SIGTERM it stops sending, returns claimed queue items, closes the DB, then exits (the agent's own grace period is 20 s)
KillSignal=SIGTERM
TimeoutStopSec=45
# The agent writes to one place only
StateDirectory=geo-mlops-edge
ReadWritePaths=/var/lib/geo-mlops-edge
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.target
- Because of
ProtectSystem=strict, the agent writes only to/var/lib/geo-mlops-edge. If the watchdir collector must move files from another folder (for example/data/incoming), add that folder toReadWritePaths=. - If you installed somewhere other than
/opt/geo-mlops/venv, changeExecStart.
Docker
Build the image with only the extras you need. Model extras pull in PyTorch and approach 1 GB, so for a device that only sends data, edge,modbus is enough.
# Dockerfile
FROM python:3.12-slim
ARG EXTRAS=edge
ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
GEO_EDGE_DATA_DIR=/var/lib/geo-mlops-edge
RUN pip install --upgrade pip \
&& pip install "geo-mlops-sdk[${EXTRAS}]>=0.2,<0.3"
# The queue and model cache must outlive the container
VOLUME ["/var/lib/geo-mlops-edge"]
RUN useradd --system --create-home --home-dir /var/lib/geo-mlops-edge geo-mlops \
&& chown -R geo-mlops:geo-mlops /var/lib/geo-mlops-edge
USER geo-mlops
EXPOSE 8600
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import httpx,sys; sys.exit(0 if httpx.get('http://127.0.0.1:8600/health', timeout=3).status_code==200 else 1)"
ENTRYPOINT ["geo-mlops-edge"]
CMD ["run"]
docker build --build-arg EXTRAS='edge,modbus' -t geo-mlops-edge .
docker build --build-arg EXTRAS='edge,gpu,yolo' -t geo-mlops-edge:yolo .
docker compose
# docker-compose.yml
name: geo-mlops-edge
services:
edge:
build:
context: .
args:
EXTRAS: ${EDGE_EXTRAS:-edge,modbus}
image: geo-mlops-edge:latest
container_name: geo-mlops-edge
restart: unless-stopped
env_file:
- path: /etc/geo-mlops/edge.env
required: false
volumes:
- edge-state:/var/lib/geo-mlops-edge # queue and model cache
- /etc/geo-mlops:/etc/geo-mlops:ro # edge.yaml is read from the default path
- ${EDGE_INCOMING:-/data/incoming}:/data/incoming # for the watchdir collector
ports:
- "${EDGE_API_PORT:-8600}:8600"
volumes:
edge-state:
name: geo-mlops-edge-state
docker compose up -d --build # build + (re)start
docker compose logs -f edge
docker compose down # stop (queue and model cache remain)
docker compose down -v # stop + delete the state volume — unsent data is lost too
- Inside the container
/etc/geo-mlops/edge.yamlis on the default lookup path, so there is nothing extra to pass. api.hostinedge.yamlmust be0.0.0.0to be reachable through the port mapping.- Do not repeat the
GEO_EDGE_*keys fromedge.envunder compose'senvironment:. Compose givesenvironment:precedence overenv_file, so after you rotate the token the old value is silently used. - The container starts with defaults even if both files are missing. In that case
link.stateinstatusstaysoffline, andcentral.configuredinGET /api/v1/settingsisfalse.
Rotating the device token
The token lasts one year, and rotation is on-site work. There is no way to deliver the new token to the device remotely.
-
On Central, in the Token management (토큰 관리) card of the device detail page, press ① Rotate (회전). The new token is shown only once, so copy it. (② Revoke (회수) only invalidates every token; it does not issue a new one.)
Token management card — ① Rotate (issue a new token) ② Revoke (invalidate every token) -
From that moment the device's next call is rejected with
401, and the agent sets the link toauth_failedand stops calling Central on its own. This keeps it from hammering the server with requests that will be rejected. Command polling stops too, so a restart command cannot reach it either. -
On the device, change
GEO_EDGE_CENTRAL__TOKENin/etc/geo-mlops/edge.envto the new value and restart the service.sudo systemctl restart geo-mlops-edge # or: docker compose up -d
Collection continues in the meantime; data accumulates in the local queue and is uploaded once the device connects with the new token.
The days left on the token are in token_expires_in_days of the fleet API (GET /api/v1/edge/fleet, GET /api/v1/edge/devices/{id}). The screen does not show it. Schedule the rotation into a site visit before it expires.