Skip to main content
Version: 10

Configure NVIDIA Jetson

Qualified Jetson Platform

JetPack 7.2.x / Jetson Linux L4T r39.2.x on Ubuntu 24.04 (Noble) is qualified for the ROS 2 Jazzy Jetson image. Newer valid L4T releases may run, but are not yet supported by this version of MoveIt Pro; the launcher displays a warning and recommends returning to JetPack 7.2.x if issues occur. Older releases and malformed L4T metadata are rejected. The image uses CUDA 13.2.1, cuDNN 9.20.0, TensorRT 10.16.2, and the runtime MOVEIT_TARGET=-jetson-cuda13.2-cudnn9 suffix. Generic arm64 images are not Jetson images.

Configuring JetPack for GPU Acceleration​

info

The following instructions assume that you already have Docker installed and that your system has internet access.

GPU acceleration for MoveIt Pro requires installing the nvidia-container-toolkit. On NVIDIA Jetson platforms with JetPack, this package is provided in an APT repository which is already configured on the system. To install and configure the package, open a terminal and run:

sudo apt update && sudo apt install nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

Confirm the host is running the qualified JetPack 7.2.x / L4T r39.2.x stack before continuing. tegrastats monitors host telemetry; it does not verify the Docker NVIDIA runtime.

Start host telemetry before starting the workload:

sudo tegrastats

While tegrastats is running, start the workload that will use the GPU and confirm that GPU and memory activity are reported. Verify the configured Docker NVIDIA runtime with the published MoveIt Pro Jetson workload during Jetson hardware qualification; a CUDA/TensorRT application smoke test requires the published image and supported hardware.

MoveIt Pro GPU Inference Availability​

MoveIt Pro detects a qualified JetPack 7.2.x / L4T r39.2.x Jetson host and sets MOVEIT_TARGET=-jetson-cuda13.2-cudnn9. A newer valid L4T release selects the same target with a compatibility warning and JetPack 7.2.x rollback guidance. This runtime target is separate from generic arm64 and from the amd64 MOVEIT_TARGET=-cuda13.2-cudnn9 image.

NVIDIA AGX Orin Onboard Storage Limitations​

The NVIDIA AGX Orin is provided with 64GB of eMMC 5.1 onboard storage. This eMMC storage is capable of a theoretical maximum linear transfer speed of 100MB/s. The NVIDIA AGX Orin Developer Kit is also provided with a PCIe x4 Gen4 M.2 slot for expansion which is capable of transfer speeds many times that of the onboard eMMC. For this reason, PickNik recommends installing a compatible M.2 NVMe storage device and configuring Docker to use this device for image storage using the instructions below.

Choosing a Drive​

For the NVIDIA AGX Orin Developer Kit, NVIDIA recommends an NVMe storage device which has the following specifications:

  • M.2 key M
  • 2280 form factor
  • PCIe Gen4 (Gen3 supported with Gen3 speeds / Gen5 supported at Gen4 speeds)
  • At least 1TB capacity

Installing the NVMe Drive​

warning

In addition to the instructions below, follow all best-practices for safe electronics handling such as anti-static procedures.

  1. Power off the NVIDIA AGX Orin Developer Kit and disconnect power and data cables.
  2. Flip the Developer Kit upside-down.
  3. Identify connector J1 using the Carrier Board Component Identification Diagram.
  4. Remove the mounting screw in the stand-off directly across from connector J1.
  5. Fully insert the M.2 NVMe drive edge connector into the carrier board connector J1 at a 45 degree angle.
  6. Gently push the drive down toward the carrier board until the semi-circular cutout in the end of the drive aligns with the stand-off from which you removed the screw in step 4.
  7. While holding the drive down, re-install the screw into the stand-off to affix the drive to the carrier board. Take care not to over-tighten this screw.
  8. Flip the Developer Kit right-side-up and reconnect power and data cables.

Formatting and Mounting​

  1. Power on the computer and open a terminal on it, locally or over SSH.
  2. Run lsblk -o NAME,SIZE,MODEL,MOUNTPOINTS. The new drive is a disk with the size of the drive you installed and no mount points, for example nvme0n1 931.5G Samsung SSD 980 1TB. The commands below call it nvme0n1; if your drive has a different name, use that name in every command. Confirm the name before you continue: steps 3 and 5 erase the drive.
  3. Run sudo fdisk /dev/nvme0n1.
    1. Type g and press Enter to create a new GPT partition table.
    2. Type n and press Enter to create a new partition.
    3. Press Enter to accept the default partition number.
    4. Press Enter to accept the default first sector.
    5. Press Enter to accept the default last sector and use the remaining drive capacity.
    6. Type w and press Enter to write the changes and exit fdisk.
  4. Run lsblk /dev/nvme0n1 and confirm that the drive now has one partition below it, named similarly to nvme0n1p1. If the partition has a different name, use that name in the next step.
  5. Run sudo mkfs.ext4 /dev/nvme0n1p1 to format the new partition as ext4.
  6. Run sudo mkdir /mnt/nvme to create the mount point for the new partition.
  7. Run lsblk -f /dev/nvme0n1 and copy the UUID of the new partition.
  8. Edit /etc/fstab with a terminal editor like vim or nano, and add a line in the format UUID=<partition_uuid> /mnt/nvme ext4 defaults 0 2, replacing <partition_uuid> with the UUID from step 7.
  9. Run sudo reboot.
  10. Once the computer has rebooted, run mountpoint /mnt/nvme and confirm that it prints /mnt/nvme is a mountpoint.

Migrating Docker Storage Location​

note

The following instructions assume that you already have Docker installed and that you carefully followed the instructions above to install and configure an NVMe drive. Each script block runs in a subshell, so a failed check stops that block without closing your terminal or SSH session.

  1. Confirm that /mnt/nvme is the intended mounted filesystem and not a symlink:

    (
    set -euo pipefail
    test -d /mnt/nvme && test ! -L /mnt/nvme &&
    mountpoint --quiet /mnt/nvme ||
    { echo "NVMe target validation failed." >&2; exit 1; }
    )
  2. Detect whether this Docker installation uses the containerd image store, then back up the current service configuration. Do not infer the backend from the Docker version: upgraded Docker 29 installations can still use the legacy image store.

    (
    set -euo pipefail
    DRIVER_STATUS="$(docker info --format '{{json .DriverStatus}}')"
    STORAGE_DRIVER="$(docker info --format '{{.Driver}}')"
    case "$DRIVER_STATUS" in
    *io.containerd.snapshotter.v1*) MIGRATE_CONTAINERD=true ;;
    *)
    test -n "$STORAGE_DRIVER" ||
    { echo "Could not determine Docker's active image store." >&2; exit 1; }
    MIGRATE_CONTAINERD=false
    ;;
    esac

    sudo install -d -m 0755 /var/backups
    BACKUP_DIR="$(sudo mktemp -d -p /var/backups moveit-pro-docker-storage.XXXXXXXX)"
    sudo chmod 0700 "$BACKUP_DIR"

    if sudo test -e /etc/docker/daemon.json; then
    sudo cp -a /etc/docker/daemon.json "$BACKUP_DIR/daemon.json"
    else
    sudo touch "$BACKUP_DIR/daemon.json.absent"
    fi

    if sudo test -e /etc/containerd/config.toml; then
    sudo cp -a /etc/containerd/config.toml "$BACKUP_DIR/containerd-config.toml"
    else
    sudo touch "$BACKUP_DIR/containerd-config.toml.absent"
    fi

    if $MIGRATE_CONTAINERD; then
    sudo touch "$BACKUP_DIR/containerd-migration.enabled"
    fi
    printf 'Configuration backup: %s\n' "$BACKUP_DIR"
    )

    Set BACKUP_DIR to the printed path in the shell you run the remaining steps from, and again if you open a new one. Each later block checks it before doing anything:

    BACKUP_DIR=/var/backups/moveit-pro-docker-storage.XXXXXXXX

    The marker records the backend reported by docker info; do not create or remove it based only on the Docker version.

  3. Record the current images and named volumes, then stop Docker and containerd:

    (
    set -euo pipefail
    sudo test -d "${BACKUP_DIR:?Set BACKUP_DIR to the path that step 2 printed.}" ||
    { echo "Backup directory $BACKUP_DIR not found." >&2; exit 1; }
    docker image ls
    docker volume ls
    sudo systemctl stop docker.socket docker.service containerd
    for unit in docker.socket docker.service containerd; do
    if sudo systemctl is-active --quiet "$unit"; then
    echo "$unit is still active; refusing to touch live data." >&2
    exit 1
    fi
    done
    )
  4. Copy the active data without deleting its source. Copy containerd only when step 2 created the migration marker. If either copy fails, the block removes everything it copied to /mnt/nvme and restarts the services, so you can rerun this step:

    (
    set -euo pipefail
    sudo test -d "${BACKUP_DIR:?Set BACKUP_DIR to the path that step 2 printed.}" ||
    { echo "Backup directory $BACKUP_DIR not found." >&2; exit 1; }
    sudo test ! -e /mnt/nvme/docker ||
    { echo "/mnt/nvme/docker already exists; refusing to merge data." >&2; exit 1; }
    if sudo test -e "$BACKUP_DIR/containerd-migration.enabled"; then
    sudo test ! -e /mnt/nvme/containerd ||
    { echo "/mnt/nvme/containerd already exists; refusing to merge data." >&2; exit 1; }
    fi
    sudo install -d -m 0711 /mnt/nvme/docker
    sudo rsync -aHAXx --numeric-ids /var/lib/docker/ /mnt/nvme/docker/ ||
    { echo "Docker data copy failed; removing the partial copy and restarting the services." >&2; sudo rm -rf /mnt/nvme/docker; sudo systemctl start containerd docker; exit 1; }

    if sudo test -e "$BACKUP_DIR/containerd-migration.enabled"; then
    sudo install -d -m 0711 /mnt/nvme/containerd
    if sudo test -d /var/lib/containerd; then
    sudo rsync -aHAXx --numeric-ids /var/lib/containerd/ /mnt/nvme/containerd/ ||
    { echo "containerd data copy failed; removing the partial copies and restarting the services." >&2; sudo rm -rf /mnt/nvme/containerd /mnt/nvme/docker; sudo systemctl start containerd docker; exit 1; }
    sudo touch "$BACKUP_DIR/containerd-data-source.present"
    fi
    fi
    )
  5. Point Docker at /mnt/nvme/docker in /etc/docker/daemon.json, keeping its other settings. If step 2 created the containerd migration marker, also point containerd at /mnt/nvme/containerd in /etc/containerd/config.toml, writing containerd's default configuration first if that file does not exist. Otherwise the block leaves the containerd root unchanged:

    (
    set -euo pipefail
    sudo test -d "${BACKUP_DIR:?Set BACKUP_DIR to the path that step 2 printed.}" ||
    { echo "Backup directory $BACKUP_DIR not found." >&2; exit 1; }
    sudo install -d -m 0755 /etc/docker
    sudo python3 - <<'EOF'
    import json, pathlib
    path = pathlib.Path("/etc/docker/daemon.json")
    text = path.read_text() if path.exists() else ""
    try:
    config = json.loads(text) if text.strip() else {}
    except ValueError as error:
    raise SystemExit(f"{path} is not valid JSON ({error}); fix it, then rerun this step.")
    if not isinstance(config, dict):
    raise SystemExit(f"{path} must contain a JSON object; fix it, then rerun this step.")
    config["data-root"] = "/mnt/nvme/docker"
    path.write_text(json.dumps(config, indent=2) + "\n")
    EOF

    if sudo test -e "$BACKUP_DIR/containerd-migration.enabled"; then
    if ! sudo test -e /etc/containerd/config.toml; then
    sudo install -d -m 0755 /etc/containerd
    containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
    fi
    sudo python3 - <<'EOF'
    import pathlib, re
    path = pathlib.Path("/etc/containerd/config.toml")
    lines = path.read_text().splitlines(keepends=True)
    # Top-level keys end at the first table header; replace any top-level root there.
    first_table = next((i for i, line in enumerate(lines) if re.match(r"\s*\[", line)), len(lines))
    kept = [line for i, line in enumerate(lines) if i >= first_table or not re.match(r"\s*root\s*=", line)]
    path.write_text('root = "/mnt/nvme/containerd"\n' + "".join(kept))
    EOF
    fi
    )
  6. Start the services and verify the migration before changing the old directories:

    (
    set -euo pipefail
    sudo test -d "${BACKUP_DIR:?Set BACKUP_DIR to the path that step 2 printed.}" ||
    { echo "Backup directory $BACKUP_DIR not found." >&2; exit 1; }
    sudo systemctl start containerd docker
    test "$(docker info --format '{{ .DockerRootDir }}')" = /mnt/nvme/docker ||
    { echo "Docker is not using the NVMe data root." >&2; exit 1; }
    if sudo test -e "$BACKUP_DIR/containerd-migration.enabled"; then
    sudo containerd config dump |
    grep -Eq "^[[:space:]]*root[[:space:]]*=[[:space:]]*[\"']/mnt/nvme/containerd[\"']" ||
    { echo "containerd is not using the NVMe root." >&2; exit 1; }
    fi
    docker image ls
    docker volume ls
    sudo systemctl --no-pager --full status containerd docker
    mountpoint --quiet /mnt/nvme
    test "$(findmnt -n -o TARGET -T /mnt/nvme/docker)" = /mnt/nvme
    if sudo test -e "$BACKUP_DIR/containerd-migration.enabled"; then
    test "$(findmnt -n -o TARGET -T /mnt/nvme/containerd)" = /mnt/nvme
    fi
    )

    Confirm that the expected images, named volumes, and volume contents are present. If validation fails, the original data still exists under /var/lib; restore the exact pre-migration configuration before restarting the services:

    (
    set -euo pipefail
    sudo test -d "${BACKUP_DIR:?Set BACKUP_DIR to the path that step 2 printed.}" ||
    { echo "Backup directory $BACKUP_DIR not found." >&2; exit 1; }
    sudo systemctl stop docker.socket docker.service containerd
    for unit in docker.socket docker.service containerd; do
    ! sudo systemctl is-active --quiet "$unit" ||
    { echo "$unit is still active; aborting rollback." >&2; exit 1; }
    done
    if sudo test -e "$BACKUP_DIR/daemon.json.absent"; then
    sudo rm -f /etc/docker/daemon.json
    else
    sudo cp -a "$BACKUP_DIR/daemon.json" /etc/docker/daemon.json
    fi
    if sudo test -e "$BACKUP_DIR/containerd-config.toml.absent"; then
    sudo rm -f /etc/containerd/config.toml
    else
    sudo cp -a "$BACKUP_DIR/containerd-config.toml" /etc/containerd/config.toml
    fi
    sudo systemctl start containerd docker
    )
  7. After the migrated system has been validated, stop the services and quarantine only the data roots that were migrated. Refuse to overwrite an earlier quarantine:

    (
    set -euo pipefail
    sudo test -d "${BACKUP_DIR:?Set BACKUP_DIR to the path that step 2 printed.}" ||
    { echo "Backup directory $BACKUP_DIR not found." >&2; exit 1; }
    mountpoint --quiet /mnt/nvme ||
    { echo "NVMe target is not mounted; refusing to quarantine data." >&2; exit 1; }
    test "$(findmnt -n -o TARGET -T /mnt/nvme/docker)" = /mnt/nvme ||
    { echo "Docker destination is not on the NVMe mount." >&2; exit 1; }
    if sudo test -e "$BACKUP_DIR/containerd-migration.enabled"; then
    test "$(findmnt -n -o TARGET -T /mnt/nvme/containerd)" = /mnt/nvme ||
    { echo "containerd destination is not on the NVMe mount." >&2; exit 1; }
    fi
    if sudo test -e "$BACKUP_DIR/daemon.json.absent"; then
    :
    else
    sudo test -f "$BACKUP_DIR/daemon.json" ||
    { echo "Docker configuration backup not found." >&2; exit 1; }
    fi
    if sudo test -e "$BACKUP_DIR/containerd-config.toml.absent"; then
    :
    else
    sudo test -f "$BACKUP_DIR/containerd-config.toml" ||
    { echo "containerd configuration backup not found." >&2; exit 1; }
    fi
    sudo test -d /var/lib/docker || { echo "Docker data root not found." >&2; exit 1; }
    if sudo test -e /var/lib/docker.pre-nvme-migration; then
    echo "Docker quarantine already exists." >&2
    exit 1
    fi
    if sudo test -e "$BACKUP_DIR/containerd-data-source.present"; then
    sudo test -d /var/lib/containerd ||
    { echo "containerd data root not found." >&2; exit 1; }
    if sudo test -e /var/lib/containerd.pre-nvme-migration; then
    echo "containerd quarantine already exists." >&2
    exit 1
    fi
    fi

    sudo systemctl stop docker.socket docker.service containerd
    for unit in docker.socket docker.service containerd; do
    ! sudo systemctl is-active --quiet "$unit" ||
    { echo "$unit is still active; refusing to move live data." >&2; exit 1; }
    done
    sudo mv /var/lib/docker /var/lib/docker.pre-nvme-migration
    if sudo test -e "$BACKUP_DIR/containerd-data-source.present"; then
    sudo mv /var/lib/containerd /var/lib/containerd.pre-nvme-migration
    fi
    sudo systemctl start containerd docker
    )

    Verify the Docker root, images, volumes, and application data again. Keep the quarantined directories until the deployment has operated successfully and you have a separate backup.

  8. To roll back after quarantine, preflight every backup and destination before stopping services. Then preserve any directories recreated by the failed configuration, restore the old data to its exact path, restore or remove each configuration file according to its marker, and restart the services:

    (
    set -euo pipefail
    sudo test -d "${BACKUP_DIR:?Set BACKUP_DIR to the path that step 2 printed.}" ||
    { echo "Backup directory $BACKUP_DIR not found." >&2; exit 1; }
    if sudo test -e "$BACKUP_DIR/daemon.json.absent"; then
    :
    else
    sudo test -f "$BACKUP_DIR/daemon.json" ||
    { echo "Docker configuration backup not found." >&2; exit 1; }
    fi
    if sudo test -e "$BACKUP_DIR/containerd-config.toml.absent"; then
    :
    else
    sudo test -f "$BACKUP_DIR/containerd-config.toml" ||
    { echo "containerd configuration backup not found." >&2; exit 1; }
    fi

    ROLLBACK_SUFFIX="failed-nvme-$(date +%Y%m%d-%H%M%S)"
    sudo test -d /var/lib/docker.pre-nvme-migration ||
    { echo "Docker quarantine not found." >&2; exit 1; }
    sudo test ! -e "/var/lib/docker.$ROLLBACK_SUFFIX" ||
    { echo "Docker failure quarantine already exists." >&2; exit 1; }
    if sudo test -e "$BACKUP_DIR/containerd-data-source.present"; then
    sudo test -d /var/lib/containerd.pre-nvme-migration ||
    { echo "containerd quarantine not found." >&2; exit 1; }
    sudo test ! -e "/var/lib/containerd.$ROLLBACK_SUFFIX" ||
    { echo "containerd failure quarantine already exists." >&2; exit 1; }
    fi

    sudo systemctl stop docker.socket docker.service containerd
    for unit in docker.socket docker.service containerd; do
    ! sudo systemctl is-active --quiet "$unit" ||
    { echo "$unit is still active; aborting rollback." >&2; exit 1; }
    done
    if sudo test -e /var/lib/docker; then
    sudo mv /var/lib/docker "/var/lib/docker.$ROLLBACK_SUFFIX"
    fi
    sudo mv /var/lib/docker.pre-nvme-migration /var/lib/docker

    if sudo test -e "$BACKUP_DIR/containerd-data-source.present"; then
    if sudo test -e /var/lib/containerd; then
    sudo mv /var/lib/containerd "/var/lib/containerd.$ROLLBACK_SUFFIX"
    fi
    sudo mv /var/lib/containerd.pre-nvme-migration /var/lib/containerd
    fi

    if sudo test -e "$BACKUP_DIR/daemon.json.absent"; then
    sudo rm -f /etc/docker/daemon.json
    else
    sudo cp -a "$BACKUP_DIR/daemon.json" /etc/docker/daemon.json
    fi
    if sudo test -e "$BACKUP_DIR/containerd-config.toml.absent"; then
    sudo rm -f /etc/containerd/config.toml
    else
    sudo cp -a "$BACKUP_DIR/containerd-config.toml" /etc/containerd/config.toml
    fi
    sudo systemctl start containerd docker
    )

Other Performance Considerations​

NVIDIA Jetson platforms are designed to be configurable for different power and performance needs. By default, the NVIDIA AGX Orin comes configured with "medium" performance and power settings which can be changed with the nvpmodel utility. For more details on this utility, see the NVIDIA Jetson Linux Developer Guide.

MAXN Power Mode​

If your environment supports higher power draw and heat output from the NVIDIA AGX Orin, you can use the command below to set the power mode to "MAXN" (or "unlimited") for better compute performance:

sudo nvpmodel -m 0

For more information on available modes, power consumption, and clock frequencies, refer to the NVIDIA Jetson Linux Developer Guide.

Jetson Clocks​

Even when using "MAXN" power mode, the clock frequencies configured for each chip or accelerator are defined as ranges. When running real-time applications on these platforms, the delays introduced when scaling clock frequencies can cause "hitches" or momentary latencies. To alleviate this, NVIDIA provides another utility called jetson_clocks which pins each chip's or accelerator's frequency to the max frequency in its range. However, the configuration changes that jetson_clocks applies are not persistent across reboots.

warning

Validate MAXN and jetson_clocks with the intended workload while monitoring tegrastats for thermal and power-limit throttling. If throttling occurs, disable persistent jetson_clocks and let NVIDIA's governor manage GPU and EMC clocks. Do not disable hardware protection without hardware-owner approval.

Run and inspect jetson_clocks manually before making it persistent:

sudo jetson_clocks
sudo jetson_clocks --show

Only after the workload completes without thermal or power-limit throttling, create /etc/systemd/system/jetson-clocks.service with:

[Unit]
Description=Jetson Clocks Service
After=nvpmodel.service

[Service]
Type=oneshot
ExecStart=/usr/bin/jetson_clocks
RemainAfterExit=true

[Install]
WantedBy=multi-user.target

Reload systemd, enable the service for boot, and start it:

sudo systemctl daemon-reload
sudo systemctl enable jetson-clocks.service
sudo systemctl start jetson-clocks.service