# Error while loading shared libraries in CI

> Fix "error while loading shared libraries" in CI: read the soname, install the package that ships it, and know when the loader path is the problem.

Source: https://latchkey.dev/learn/failures/error-while-loading-shared-libraries-in-ci  
Updated: 2026-09-19

"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.

```Actions log, test step
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
```

## 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

1. Take the soname from the message, including its version suffix.
2. Resolve it to a package with `apt-file search`, or from the distribution's filelist page.
3. 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.

```.github/workflows/ci.yml
- name: Browser system libraries
  run: |
    sudo apt-get update
    sudo apt-get install -y libgbm1 libnss3 libgtk-3-0t64 libasound2t64
```

### Let 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.

```Terminal
npx playwright install --with-deps chromium
# Cypress
sudo apt-get install -y libgtk-3-0t64 libgbm1 libnotify4 libnss3 libxss1 xvfb
```

### Point 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.

```.github/workflows/ci.yml
- run: echo "LD_LIBRARY_PATH=$(python3 -c 'import site;print(site.getsitepackages()[0])')/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV
```

### Move 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.

```.github/workflows/ci.yml
jobs:
  e2e:
    runs-on: ubuntu-latest
    container: mcr.microsoft.com/playwright:v1.56.0-noble
```

## 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-deps` command where the tool publishes one.
- Run `ldd` on 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.

## 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.

```Terminal
ldd ./your-binary | grep "not found"
# for a Python extension
python3 -c "import ctypes; ctypes.CDLL('libgbm.so.1')"
```

## Find 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.

```Terminal
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.1
```

## When 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.

```.github/workflows/ci.yml
- 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 ldconfig
```

## Version 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.

## FAQ

### What does "error while loading shared libraries" mean?

The dynamic loader could not find a library the binary was linked against, so the program was never started. The name after the colon is the exact file it wanted, including the ABI version suffix. It is not a problem with your code, and the program produced no output of its own because it never ran.

### How do I find which package provides a .so file?

Use `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?

Because a headless browser needs a large slice of a desktop system, and a slim image ships none of it. Chromium alone pulls in NSS, GTK, X11 and graphics libraries. Use `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?

It works and it is worth avoiding. An unmanaged file in a package-managed directory conflicts with the next upgrade, and a library copied from a different distribution release can depend on a libc newer than the one on the machine. Install the package, or put your copy on `LD_LIBRARY_PATH` instead.

## References

- [ld.so(8): how the dynamic loader finds libraries](https://man7.org/linux/man-pages/man8/ld.so.8.html)
- [Playwright: system requirements and install-deps](https://playwright.dev/docs/ci)
- [Ubuntu package contents search](https://packages.ubuntu.com/)
- [GitHub-hosted runners: standard runner specifications](https://docs.github.com/en/actions/reference/runners/github-hosted-runners)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
