Complete headless Jetson Orin Nano setup (NVMe boot + GNOME & XFCE VNC)

I have written a blog for fully headless setup, covering NVMe SSD flashing and two stable ways to get remote GUI access: GNOME via x11vnc and XFCE fallback. Blog link: Jetson Orin Nano Headless Setup: NVMe Boot, SDK Flashing, and VNC (GNOME & XFCE) - Nandan Manjunatha

Copy-pasting my blog content so that the info is not lost in the future:

This post walks through SSD-based flashing using NVIDIA SDK Manager with a host<->target model, then covers two proven strategies for remote desktop access: a GNOME setup using x11vnc, and a lightweight XFCE fallback using TigerVNC. Expect explicit steps, rationale for each design choice, and configurations that survive reboots and updates, no monitor, no keyboard, and no fragile shortcuts.

Audience & Assumptions:
This guide assumes familiarity with Linux, SSH, and systemd. It is written for developers running Jetson Orin Nano headlessly (no monitor/keyboard) who want reliable remote GUI access without fragile workarounds.

Part 1:

Flashing OS onto Jetson Headlessly with an SSD

This section explains how to flash and boot an NVMe SSD on a Jetson Orin Nano without attaching a monitor or keyboard. The process uses NVIDIA SDK Manager , which enforces a strict host–target model. Confusing the two is the most common cause of failure, so we state them clearly.

Before proceeding, I would recommend to watch this setup guide from DronBot Workshop on YouTube, which covers the same ground visually: YouTube Link . Even I followed the same until acquiring the IP address of the Jetson after flashing.

Host and Target Overview

Host Machine (Required)

  • Purpose: Flashes the Jetson and installs NVIDIA SDK components

  • System: Ubuntu 20.04 or 22.04 (x86_64 PC or laptop)

  • Software: NVIDIA SDK Manager

  • Connectivity: USB-C (data cable) during flashing, Ethernet used later for SDK installation


Target Device (Jetson)

  • Device: NVIDIA Jetson Orin Nano Developer Kit

  • Storage: NVMe SSD (M.2 2230 or 2280, PCIe Gen3 only; minimum 256 GB recommended)

  • Flashing state: Recovery mode

  • Peripherals: No display or keyboard required (headless)

  • Networking: Ethernet required after flashing

The host only orchestrates the process. The target is the device being flashed and configured.

Step 1: Install NVMe SSD on the Target

Power off the Jetson Orin Nano. Install the NVMe SSD into one of the M.2 slots on the underside of the carrier board and secure it with the provided screw.

This SSD will become the boot device. No microSD card is involved.

Step 2: Put the Target into Recovery Mode

External Image

To allow the host to flash the target, recovery mode is mandatory.

  1. Locate the 14-pin button header on the Jetson carrier board.

  2. Short FC REC and GND (pins 2 and 3) using a jumper.

  3. Leave the jumper connected.

  4. Power on the Jetson.

If recovery mode is not active, the host will not detect the target.

Step 3: Prepare the Host Machine

On the host (Ubuntu PC):

  1. Install NVIDIA SDK Manager (.deb package).

  2. Disable sleep and screen blanking.

  3. Ensure internet connectivity.

Nothing in this step affects the Jetson directly.

Step 4: Flash the Target from the Host

External Image

All actions below happen on the host, but they operate on the target.

  1. Launch SDK Manager on the host and log in.

  2. Connect the Jetson (in recovery mode) to the host using USB-C.

  3. Power on the Jetson.

  4. SDK Manager should detect the device:

    • Select Jetson Orin Nano 8GB Developer Kit
  5. Accept the license agreement.

  6. Click Download and Flash.

  7. When prompted:

    • Choose Preconfig (recommended for headless use), or

    • Choose Runtime if you prefer first-boot setup.

  8. Select nvme as the target storage device.

  9. Start flashing and wait.

Do not disconnect power or USB. If this fails, you start over.

Step 5: First Boot and Network-Based SDK Installation

  1. After flashing completes:

    • Power off the Jetson

    • Remove the recovery jumper. I used female dupont connectors for easy handling.

  2. Power on the Jetson normally (no USB flashing cable required anymore).

  3. Connect the Jetson to your network using Ethernet. This is mandatory, the remaining SDK components are installed over the network.

  4. Determine the Jetson’s IP address without using a monitor or keyboard.

    Since this is a headless setup, do not rely on local login. Use one of the following host-side methods:

    Scan the local subnet (recommended)

    1. On the host, identify the active Ethernet interface and subnet:
    ip addr show eth0
    
    

    Example output:

    inet 10.42.0.1/24
    
    
    1. Scan the subnet for live devices: Install nmap if not already present: sudo apt install nmap
    nmap -sn 10.42.0.0/24
    
    

    Identify the Jetson by vendor or as a newly appeared IP (typically 10.42.0.xxx).

  5. Return to NVIDIA SDK Manager on the host.

  6. Provide:

    • Target IP address

    • Target username

    • Target password

  7. Resume and complete SDK component installation.

Once this step finishes, the Jetson is fully operational and no longer depends on the host.

Note: The same IP address is used for SSH, SCP, and VNC access going forward.


Part 2:

Setting Up VNC for Remote Desktop Access * GNOME Edition

External Image

Running GNOME headlessly on a Jetson-class device is non-trivial. GNOME expects a real display, valid EDID, and an active login session. GNOME cannot run headlessly without a valid display pipeline. This setup mirrors the real :0 display using x11vnc, which is the only method that remains stable across reboots and driver updates. This guide shows a working, production-grade setup using x11vnc, mirroring the real GNOME display over VNC, no hacks, no virtual desktops. This method was recommended by NVIDIA engineers themselves on their developer forums.

This Nvidia forum post was the inspiration for this method, but we are not doing it exactly the same way. Here they have used monitor for a setting change, we are doing it completely headless.

The approach is explicit:

  • Force GNOME to start with a valid EDID

  • Ensure a real :0 display exists

  • Attach x11vnc to that display at boot

Prerequisites

  • GNOME desktop installed and enabled

  • Root access

  • SSH access (since this is headless)

  • A known-good EDID from any working monitor

  • A VNC client on your host machine

This setup is commonly used on Jetson devices, but applies to any NVIDIA system running GNOME with the proprietary driver.

Step 1: Install and Initialize x11vnc

Install x11vnc and set a VNC password:

sudo apt update
sudo apt install x11vnc -y
x11vnc -storepasswd

x11vnc does not create a display. It attaches to an existing one. Everything that follows exists to guarantee that display.

Step 2: Force Graphical Boot Target

GNOME must start at boot, even without a monitor.

sudo systemctl set-default graphical.target
sudo reboot

If this step is skipped, x11vnc will start successfully, and attach to nothing.

Step 3: Extract EDID from a Working Monitor (Host Side)

On any Linux system with a monitor attached (your host machine):

xrandr --props

External Image

Locate the EDID: block, copy only the hex values, and paste them into a file without spaces or line breaks:

nano edid.hex

External Image

Convert it to binary:

xxd -r -p edid.hex EDID.bin

Verify the EDID:

edid-decode EDID.bin

External Image You should see a valid output describing the monitor capabilities.

Step 4: Install EDID on the Target (Jetson)

⚠️ Use an EDID from a monitor that supports the resolution you intend to use. Invalid EDIDs will cause GDM to fail silently.

Copy the EDID to the Jetson:

scp EDID.bin YOUR_USER@jetson:/tmp

Move it into firmware location:

sudo mkdir -p /lib/firmware/edid
sudo cp /tmp/EDID.bin /lib/firmware/edid/EDID.bin

Step 5: Replace Xorg Configuration (Critical)

Back up the existing config:

sudo cp /etc/X11/xorg.conf /etc/X11/xorg.conf.backup
sudo nano /etc/X11/xorg.conf

Replace the entire file with the following config:

If /etc/X11/xorg.conf does not exist, create it. Jetson images often rely on autogenerated defaults.

Section "ServerLayout"
    Identifier     "Layout0"
    Screen      0  "Screen0" 0 0
    InputDevice    "Keyboard0" "CoreKeyboard"
    InputDevice    "Mouse0" "CorePointer"
EndSection

Section "Module"
    SubSection     "extmod"
        Option         "omit xfree86-dga"
    EndSubSection
    Load           "glx"
    Disable        "dri"
EndSection

Section "InputDevice"
    Identifier     "Keyboard0"
    Driver         "kbd"
EndSection

Section "InputDevice"
    Identifier     "Mouse0"
    Driver         "mouse"
    Option         "Protocol" "auto"
    Option         "Device" "/dev/mouse"
    Option         "Emulate3Buttons" "no"
    Option         "ZAxisMapping" "4 5"
EndSection

Section "Device"
    Identifier     "Tegra0"
    Driver         "nvidia"
    Option         "AllowEmptyInitialConfiguration" "true"
    Option         "ConnectedMonitor" "DFP-0"
    Option         "CustomEDID" "DFP-0:/lib/firmware/edid/EDID.bin"
    Option         "UseDisplayDevice" "DFP-0"
    Option         "IgnoreEDIDChecksum" "DFP-0"
    Option         "ModeValidation" "AllowNonEdidModes"
EndSection

Section "Monitor"
    Identifier     "Monitor0"
    VendorName     "FakeVendor"
    ModelName      "Fake1920x1080"
    Option         "DPMS" "false"
EndSection

Section "Screen"
    Identifier     "Screen0"
    Device         "Tegra0"
    Monitor        "Monitor0"
    DefaultDepth    24
    SubSection     "Display"
        Depth       24
        Modes      "1920x1080"
    EndSubSection
EndSection

Restart GNOME:

sudo systemctl restart gdm3

Sanity check:

DISPLAY=:0 xrandr

If this fails, VNC will never work. Fix this first.

Step 6: Enable GNOME Auto-Login (Mandatory)

x11vnc requires an active user session. No login → no desktop.

Edit GDM configuration:

sudo nano /etc/gdm3/custom.conf

Enable auto-login:

[daemon]
AutomaticLoginEnable=true
AutomaticLogin=YOUR_USERNAME

Reboot:

sudo reboot

Verify again:

DISPLAY=:0 xrandr

Step 7: Create a systemd Service for x11vnc

Create the service file:

sudo nano /etc/systemd/system/x11vnc.service

Paste the following:

[Unit]
Description=Start x11vnc at startup
Requires=display-manager.service
After=display-manager.service

[Service]
Type=simple
User=YOUR_USERNAME
ExecStart=/usr/bin/x11vnc -auth guess -display WAIT:0 -loop -usepw -forever -rfbport 5900
Restart=on-failure

[Install]
WantedBy=graphical.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable x11vnc.service
sudo systemctl start x11vnc.service

Finally, Connect via VNC Viewer

Install a VNC viewer on your host machine (e.g., tigervnc-viewer, RealVNC, etc.) I recommend tigervnc-viewer for its simplicity and performance. Or RealVNC for its user-friendly interface.

Recommended: SSH Tunnel + Local VNC Viewer

From your host machine:

ssh -N -L 5900:localhost:5900 yourusername@JETSON_IP
vncviewer localhost:5900

You should now see the real GNOME desktop, hardware-accelerated, exactly as if a monitor were attached.

If you see a blank screen or errors, revisit the EDID and Xorg configuration steps. These are proven to be the most fragile parts of this setup. Many people reported rectifying in EDID and xorg.conf fixed their issues.


Part 3:

Setting Up VNC for Remote Desktop Access * XFCE Edition

External Image

This is completely optional. If you prefer GNOME, skip this section. I keep it here for completeness and serves as a backup plan if GNOME VNC setup fails by some updates in future.

If you want reliable headless VNC without GNOME’s complexity, XFCE is the pragmatic choice. XFCE is lightweight, predictable, and works cleanly with TigerVNC using a user-level systemd service. No EDID hacks, no display manager dependencies, no GPU quirks.

This guide sets up a persistent, headless XFCE desktop accessible over VNC, designed to survive reboots and run without any local login.

Why XFCE + TigerVNC

  • No dependency on a physical display

  • No Xorg/Wayland/GDM edge cases

  • Clean separation from system display managers

  • Works perfectly over SSH tunnels

  • Ideal for Jetson, servers, and lab machines

If you need stability over aesthetics, this is the correct stack.

Step 1: Install XFCE (Target Machine)

Install the XFCE desktop environment:

sudo apt update
sudo apt install -y xfce4 xfce4-goodies

Do not install a display manager. We are running XFCE entirely inside VNC.

Step 2: Install TigerVNC

sudo apt install -y tigervnc-standalone-server tigervnc-common

Set a VNC password:

vncpasswd

This creates ~/.vnc/passwd, which TigerVNC will use for authentication.

Step 3: Quick Sanity Test

Start a temporary VNC server:

tigervncserver :1

If this launches without errors, the core components are working. Stop it afterward:

tigervncserver -kill :1

XFCE runs on a separate X display (:1) and does not interfere with GNOME (:0). Both can coexist safely. Also that the port 5900 is for GNOME VNC, while 5901 is for XFCE VNC. Adjust accordingly based on which desktop you are using.

Step 4: Disable Legacy VNC Startup Scripts

Remove or empty any existing VNC startup files to avoid race conditions with systemd:

rm -f ~/.vnc/xstartup

TigerVNC will be launched and managed only via systemd from this point onward.

Step 5: Create a User-Level systemd Service

Create the user systemd directory if it doesn’t exist:

mkdir -p ~/.config/systemd/user
nano ~/.config/systemd/user/tigervnc.service

Paste the following service definition (adjust username if needed):

[Unit]
Description=TigerVNC server (Xfce headless)
After=network.target

[Service]
Type=simple
Environment=DISPLAY=:1

ExecStart=/usr/bin/Xtigervnc \
  :1 \
  -localhost \
  -SecurityTypes VncAuth \
  -rfbauth /home/yourusername/.vnc/passwd \
  -geometry 1920x1080

ExecStartPost=/bin/sh -c 'sleep 2; DISPLAY=:1 dbus-launch --exit-with-session xfce4-session & disown'

Restart=on-failure
RestartSec=3

[Install]
WantedBy=default.target

Why this works

  • Xtigervnc creates a real X server

  • XFCE runs inside that server

  • dbus-launch ensures session services work correctly

  • No dependency on login managers or GPUs

Step 6: Enable User Lingering (Critical)

By default, user services do not start unless the user logs in. Headless systems need lingering enabled.

sudo loginctl enable-linger yourusername

Verify:

loginctl show-user yourusername | grep Linger

Expected output:

Linger=yes

If this is not set, your VNC server will not start after reboot.

Step 7: Enable and Start the Service

Reload user services and enable TigerVNC:

systemctl --user daemon-reload
systemctl --user enable tigervnc
systemctl --user start tigervnc

Check status:

systemctl --user status tigervnc

Reboot to confirm persistence:

sudo reboot

Step 8: Access VNC Securely Over SSH

Same here, use an SSH tunnel to secure your VNC connection.

From your host machine:

ssh -N -L 5901:localhost:5901 yourusername@JETSON_IP
vncviewer localhost:5901

This keeps VNC off the network and avoids exposing port 5901.


Final Notes

Troubleshooting

If you encounter issues, First do a google search to see if there are nvidia forum posts. Keep taking LLMs help as last resort. as often I see it can go crazy and overcomplicate things.

Ports & Displays

Display :0  → GNOME + x11vnc → TCP 5900
Display :1  → XFCE + TigerVNC → TCP 5901

Security Note

Do not expose VNC ports directly to the network. Always use SSH tunneling.

2 Likes

Thanks for your sharing to the community!

Thank you for this excellent post.

I followed “Part 2” and I am able to connect from Windows into Jetson using free version of RealVNC client.
However I cannot Copy from Windows and Paste into Jetson. It works the other way around, as well as on Jetson itself.
This is kind of deal breaker for me since I am developing on Jetson for the most part.
It is not a problem with RealVNC which has setting to enable Copy/Paste and it is turned on. I am also using the same instance of RealVNC to connect to Raspberry Pi and I can copy both ways.

I looked into “autocutsel“ but I was not able to make it work in this scenario.

Any ideas would be greatly appreciated.

UPDATE:
I discovered that Paste from Client to Remote works selectively for few applications. In my case it does work in Chromium and Mousepad. It does not work for Terminal, Text Editor, Libre Office Apps, …
Also tried TightVNC cilent, same behavior.