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.

FileContentsPermissions
/etc/geo-mlops/edge.yamlSite setup — device name, collectors, retention limitsReadable
/etc/geo-mlops/edge.envSecrets — one GEO_EDGE_*=value per line. At least GEO_EDGE_CENTRAL__TOKEN600, owner only

systemd

  1. 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'
  2. 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
  3. Save the unit below as /etc/systemd/system/geo-mlops-edge.service and 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 to ReadWritePaths=.
  • If you installed somewhere other than /opt/geo-mlops/venv, change ExecStart.

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.yaml is on the default lookup path, so there is nothing extra to pass.
  • api.host in edge.yaml must be 0.0.0.0 to be reachable through the port mapping.
  • Do not repeat the GEO_EDGE_* keys from edge.env under compose's environment:. Compose gives environment: precedence over env_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 status tells 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.

  1. 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)
  2. From that moment the device's next call is rejected with 401, and the agent sets the link to auth_failed and 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.

  3. On the device, change GEO_EDGE_CENTRAL__TOKEN in /etc/geo-mlops/edge.env to 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.

Written for the platform as of 2026-09-21.

© Geo-MLOps