Rendering FlightGear headless

Last updated · how this page is sourced

If you build scenery, at some point you need to look at it, and the machine that built it may have no display and no graphics card: a build server, a CI job, a Docker container on a laptop. Running FlightGear headless is the answer, and it works: no GPU, no X session, no window, and a frame at the end of it.

It is also how the renders on this site were made: inside a Docker container on an Apple Silicon Mac, where no graphics card is reachable at all. The recipe below ran on 23 August 2026 and produced those images; the capture script is published with the build record.

The short version

Xvfb :99 -screen 0 1024x576x24 &
export DISPLAY=:99 LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe

dbus-run-session -- fgfs \
  --fg-root=/usr/share/games/flightgear \
  --fg-scenery=/path/to/your/scenery \
  --aircraft=ufo \
  --airport=KSFO --altitude=2000 \
  --disable-terrasync --disable-sound --disable-ai-models \
  --disable-ai-traffic --disable-real-weather-fetch \
  --disable-random-objects --disable-random-vegetation \
  --disable-clouds --disable-clouds3d --disable-splash-screen \
  --timeofday=noon --visibility=40000 --geometry=1024x576 \
  --prop:/sim/rendering/multi-sample-buffers=0 \
  --prop:/sim/rendering/shader-effects=0 \
  --prop:/sim/rendering/quality-level=0 &

sleep 15
import -window root -display :99 frame.png

Two of those lines are the whole difficulty, and neither is obvious.

1. It needs a D-Bus session or it aborts instantly

Without one, fgfs dies in under a second; not on a graphics error, on an assertion from a library you did not know it used:

dbus[11]: arguments to dbus_connection_set_exit_on_disconnect() were incorrect,
assertion "connection != NULL" failed in file ../../../dbus/dbus-connection.c line 3159.
Aborted

FlightGear’s Qt front end wants a session bus. Containers do not have one. Install dbus and dbus-x11, then launch under dbus-run-session, which creates a bus for the life of the command and tears it down after.

2. The default aircraft will not finish a frame

With the default c172p, the log ran to 13.5 seconds, reached the scenery, and then stopped. Nothing more was written for three minutes; the process stayed alive at 100% of one core and never produced a frame. It is not hung on anything you can see in the log: software rasterisation is simply too slow for a fully modelled cockpit interior.

--aircraft=ufo has no cockpit model. The same setup rendered in under fifteen seconds.

If you want an aircraft in shot, pick the simplest one that will do, and expect first-frame time to scale with how much of it is modelled.

Checking software OpenGL is actually active

glxinfo -B | grep -E "OpenGL (vendor|renderer|version)"

What you want to see:

OpenGL vendor string: Mesa/X.org
OpenGL renderer string: llvmpipe (LLVM 15.0.6, 128 bits)
OpenGL version string: 4.5 (Compatibility Profile) Mesa 22.3.6

llvmpipe is Mesa’s software rasteriser. OpenGL 4.5 in software is more than FlightGear needs. If the renderer says anything else, or glxinfo fails, the display or the driver is not set up and fgfs will fail with an inability to create a window rather than anything more helpful.

The packages

On Debian 12, this is the whole list:

apt-get install --no-install-recommends \
  flightgear flightgear-data-base flightgear-data-models \
  xvfb x11-utils imagemagick \
  libgl1-mesa-dri mesa-utils \
  dbus dbus-x11

Two things worth knowing on Debian: fgfs installs to /usr/games, which is not on PATH for a container’s root user, and FG_ROOT is /usr/share/games/flightgear. Without flightgear-data-models the scenery loads but every windsock, beacon and taxiway sign logs a “file not found”; harmless, and a lot of noise.

The image comes to about 5.6 GB, most of it the 1.3 GB base data package.

Render a control frame

This is the part people skip, and it is what makes the other frames mean anything. Point FlightGear at an empty scenery directory and render the same view:

mkdir -p /tmp/empty/Terrain /tmp/empty/Objects
# ... same command, --fg-scenery=/tmp/empty

At KSFO that gives open ocean to the horizon, because FlightGear’s data package ships terrain for three buckets only; w030n60, w160n10 and w160n20; and San Francisco is not among them. Anything visible in your real render therefore came out of your build and not out of the box.

Without that check, a screenshot of terrain proves nothing: bundled scenery, a TerraSync download and your own tiles all look like terrain.

Knowing when a frame is ready

Sleeping for a fixed time works and wastes time. A better test is to capture repeatedly and look at how many distinct colours the image has:

import -window root -display :99 /tmp/f.png
convert /tmp/f.png -format "%[mean] %k" info:

A blank or still-loading frame comes back with a handful of colours. The rendered frames here had between eight and nine thousand. Polling for a colour count above a few hundred is a reliable “the world is drawn” signal and takes seconds rather than minutes.

What this does not give you

  • Frame rates. llvmpipe is for producing an image, not for measuring performance. Nothing about timing here transfers to real hardware.
  • Shader-dependent visuals. The recipe turns shader effects off. Anything whose appearance depends on them will not look the way it does on a GPU.
  • Interaction. This renders a frame from a fixed position. Flying it is a different exercise.
Versions this was run against. FlightGear 2020.3.16 from Debian 12, Mesa 22.3.6, on an arm64 host with no GPU passed through. It read scenery containing BTG version 10 tiles, which is worth noting because support for that version landed in SimGear three years after 2020.3 shipped: it was expected to fail and did not.