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, something has to start it again. Pick either systemd or Docker.
Either way there are two 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=Geo-MLOps 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 finishes the chunk in flight, tidies the queue, 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
statustells you there is nowhere to send to.
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 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 fleet API reports the days left on the token (token_expires_in_days). So that expiry does not arrive unannounced, schedule the rotation into a site visit before it expires.