Error while loading shared libraries in CI
"error while loading shared libraries" in CI means the dynamic loader could not find a library the binary was linked against, so the process died before running a line of its own code. On a GitHub Actions runner that is nearly always a base image that never shipped it: install the package that ships that exact soname, or put the copy you already have on the loader path.


What this error means
A program that worked on your machine exits immediately in CI with one line: "renderer: error while loading shared libraries: libgbm.so.1: cannot open shared object file: No such file or directory", and the exit code is 127. The name before the colon is the program, the name after it is the library, and nothing in between is yours. A second form says the same thing from inside a running process: Python raises OSError naming the file, Node throws from a native addon, and a browser driver reports that the browser closed unexpectedly with the loader message buried in its own log. Both mean a file the loader needed was not on the machine or not on its path.
libgbm.so.1 => /lib/x86_64-linux-gnu/libgbm.so.1 (0x00007f7d4feea000)
/home/runner/renderer: error while loading shared libraries: libgbm.so.1: cannot open shared object file: No such file or directoryReproduced on a Latchkey runner
libgbm.so.1 => /lib/x86_64-linux-gnu/libgbm.so.1 (0x00007f7d4feea000)
/home/runner/renderer: error while loading shared libraries: libgbm.so.1: cannot open shared object file: No such file or directory
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)
[latchkey-bash-wrapper] installed package: libgbm1
renderer starting
renderer okInstalled the apt package that ships the missing shared library and retried
Read the soname, not the program name
The useful part of the message is the library name, and it is precise on purpose. libgbm.so.1 is a soname: the library name plus the ABI version the binary was built against. libgbm.so without the suffix is a different file, shipped by a different package, and installing it does not satisfy the loader.
ldd on the binary tells you the whole list and marks the ones that are missing, which is faster than failing one library at a time. Run it in the failing job the first time and you get every missing name in one go.
ldd ./your-binary | grep "not found"
# for a Python extension
python3 -c "import ctypes; ctypes.CDLL('libgbm.so.1')"Common causes
The image never shipped the library
The common case, and the reason it always happens in CI and never locally. A slim container, a distroless base or a minimal runner image carries the language runtime and little else, while your laptop accumulated a desktop's worth of graphics and font libraries years ago. Headless browsers are the usual casualty because they need a surprising amount of a desktop.
The install put the file somewhere the loader does not look
A pip wheel with a bundled native library, a manually extracted tarball, or a CUDA toolkit under /usr/local all place real files outside the default search path. The library exists and the loader still reports it missing, which is what makes this variant confusing.
The soname is a version the distribution cannot provide
A binary built on an older distribution asks for an older ABI, for example libssl.so.1.1 on a release that ships OpenSSL 3. No package satisfies it, and forcing an older library alongside the newer one breaks other things instead.
The development package went in instead of the runtime one
Packages ending in -dev ship headers and an unversioned symlink for linking, not the versioned runtime file. Installing one and finding the error unchanged is a common half hour, and in our experience it is the single most frequent wrong turn on this failure.
How to fix it
Install the package that ships the exact soname
- Take the soname from the message, including its version suffix.
- Resolve it to a package with
apt-file search, or from the distribution's filelist page. - Install that package in a step before the one that fails, and keep the list in the workflow rather than in an image nobody rebuilds.
- name: Browser system libraries
run: |
sudo apt-get update
sudo apt-get install -y libgbm1 libnss3 libgtk-3-0t64 libasound2t64Let the tool install its own dependency set
Playwright, Cypress and the browser drivers all publish the list, and their own command is more reliable than a copied one because it tracks the image they support. Use it where it exists, and keep your own list only for what the tool does not cover.
npx playwright install --with-deps chromium
# Cypress
sudo apt-get install -y libgtk-3-0t64 libgbm1 libnotify4 libnss3 libxss1 xvfbPoint the loader at a library you already have
When ldd shows the file on disk but the loader disagrees, the fix is the search path rather than another install. Prefer LD_LIBRARY_PATH for one step and an ld.so.conf.d entry for a machine you control.
- run: echo "LD_LIBRARY_PATH=$(python3 -c 'import site;print(site.getsitepackages()[0])')/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENVMove to an image that already has them
When the list is long and the tool publishes an image, use the image. The official Playwright and Cypress containers exist because the dependency set is large enough that maintaining it yourself is a recurring cost rather than a one-off.
jobs:
e2e:
runs-on: ubuntu-latest
container: mcr.microsoft.com/playwright:v1.56.0-nobleFind the package that ships the file
One soname is shipped by one package, and you can look the mapping up rather than guess it. apt-file search answers it on the runner, and the Ubuntu package filelist pages answer it from a browser without installing anything.
The mapping is worth doing properly, because the guesses are wrong in a specific way: the package named after the library is often the development package, which ships the unversioned symlink and the headers and not the runtime file your binary asked for. libgl1 ships libGL.so.1; libgl-dev does not.
sudo apt-get install -y apt-file && sudo apt-file update
apt-file search libgbm.so.1
# libgbm1: /usr/lib/x86_64-linux-gnu/libgbm.so.1When the file is there and the loader still cannot see it
A library installed outside the standard directories is invisible until you say where it is. The loader searches the paths compiled into the binary, then LD_LIBRARY_PATH, then the cache ldconfig maintains. A wheel that ships its own .so in a site-packages directory, or a CUDA install under /usr/local/cuda, is in none of those by default.
Set LD_LIBRARY_PATH for the step, or add the directory to ld.so.conf.d and run ldconfig once. Copying the file into /usr/lib also works and is the one to avoid: it puts an unmanaged file where the package manager expects to own everything, and the next upgrade will disagree with you.
- run: echo "LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH" >> $GITHUB_ENV
# or, once, for the whole machine
sudo sh -c 'echo /usr/local/cuda/lib64 > /etc/ld.so.conf.d/cuda.conf'
sudo ldconfigVersion notes: the soname that cannot be satisfied
Some of these are not installable, and recognising them early saves an afternoon. A binary built against libssl.so.1.1 cannot run on Ubuntu 24.04, which ships OpenSSL 3 and has no package that provides the older soname. The answer there is a rebuilt binary or an older base image, not an install command.
Latchkey's own soname table makes the same call explicitly and is a useful list of the hard cases. It maps the browser and graphics set, libgbm.so.1, libGL.so.1, the GTK 3 pair and the nine NSS libraries, and it deliberately declines libssl.so.3, libcuda.so.1 and libstdc++.so.6, on the grounds that the first is not satisfiable from the allowlisted packages, the second needs a real driver, and satisfying the loader without one "would convert a clear load error into a confusing runtime CUDA failure".
What the runner does about it
Latchkey detects this failure as SHARED_LIB_MISSING at confidence 0.87, captures the soname out of the message, resolves it to a package through that table, installs it and retries the step. On the recorded run above the installed package line names libgbm1 and the retried binary printed the two lines it was written to print.
Where it stops is as useful as where it works. A soname that is not in the table fails the install allowlist check at dispatch time and the failure passes through unrepaired, which is the deliberate design: the allowlist is the safety net, because the soname in the message is text from a process that already failed.
How to prevent it
- Install system libraries in an explicit step, not as a side effect of something else.
- Pin the tool's own
install-depscommand where the tool publishes one. - Run
lddon anything you build or download in CI, once, and record what it needs. - Prefer the vendor image when the dependency list is longer than a line.
Frequently asked questions
What does "error while loading shared libraries" mean?
How do I find which package provides a .so file?
apt-file search libfoo.so.1 on Debian and Ubuntu, dnf provides */libfoo.so.1 on Fedora, or the distribution's package filelist pages in a browser. Match the versioned name exactly: the package named after the library is often the development package, which does not ship the runtime file.Why does Playwright or Puppeteer fail with libnss3.so in a container?
npx playwright install --with-deps, or the vendor image, rather than adding packages one failure at a time.Can I just copy the .so file onto the runner?
LD_LIBRARY_PATH instead.