# tianon/gosu: dropping root in a container without su or sudo

> gosu is a small Go binary that changes user and group, then execs your command so nothing stays in the process tree. It is built for one job: stepping down from root in a container ENTRYPOINT.

**tianon/gosu** — Simple Go-based setuid+setgid+setgroups+exec

- Repository: https://github.com/tianon/gosu
- Stars: 5,011 · Forks: 355
- Language: Shell
- License: Apache-2.0
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/tianon-gosu

## The problem gosu solves is narrower than its name suggests

The README opens by naming the annoyance directly: su and sudo have "very strange and often annoying TTY and signal-forwarding behavior." That is the whole motivation. In a container, the entrypoint usually starts as root, needs to drop to a normal user for the actual workload, and then should get out of the way. su and sudo both stay in the process tree, which means signals sent to PID 1 are not necessarily delivered to the application, and the TTY story gets confusing.

gosu targets that exact case. The README says the core use case is stepping down from root to a non-privileged user during container startup, specifically in the ENTRYPOINT. It is not a general privilege-escalation tool, and the README states plainly that it is not intended as a sudo replacement. sudo forks and execs for reasons documented elsewhere; gosu only execs.

The audience is therefore container image authors, not desktop users. If you are writing a Dockerfile and your application should not run as root, gosu is the tool this project was written for.

## How gosu changes user and then disappears from the process tree

The mechanism is stated in one sentence in the README: once the user and group are processed, gosu switches to that user, execs the specified process, and gosu itself is no longer resident or involved in the process lifecycle. That is the design. There is no supervisor, no signal relay, no child process to reap.

The user and group parsing is not gosu's own invention. The README says the core is stolen directly from how Docker/libcontainer starts an application inside a container, and that it uses the /etc/passwd processing code directly from libcontainer's codebase. The go.mod file confirms the dependency: github.com/moby/sys/user v0.1.0. That dependency is the reason the README can claim exact 1:1 parity with Docker's own --user flag. If you have ever written --user 1000:1000 in a docker run command, that is the same parsing path gosu uses.

The usage string is short enough to quote in full. gosu takes a user-spec and a command, where the user-spec can be a name, a name:group pair, or numeric IDs. The README's own examples include gosu tianon bash, gosu nobody:root bash -c 'whoami && id', and gosu 1000:1 id.

## Installing gosu and using it in a Dockerfile ENTRYPOINT

The README gives high-level installation steps rather than a package manager command. You download the binary named for your architecture, download its detached signature, fetch the maintainer's public key, verify, and mark it executable. The README points to INSTALL.md for explicit Dockerfile instructions.

The architecture suffix comes from dpkg. The README uses this expression to build the filename:

```bash
gosu-$(dpkg --print-architecture | awk -F- '{ print $NF }')
```

So on amd64 you fetch gosu-amd64, and on arm64 you fetch gosu-arm64. The same expression with .asc appended gives you the signature file. Then fetch the signing key and verify:

```bash
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4
gpg --batch --verify gosu.asc gosu
chmod +x gosu
```

After that, gosu is a binary you place on PATH, usually /usr/local/bin/gosu. A first real use is to run a shell as another user and confirm the identity switch. The README's example passes a command that prints both whoami and id:

```bash
gosu nobody:root bash -c 'whoami && id'
```

You should see nobody printed by whoami, and id should report the numeric uid and gid for nobody with root as the group. Numeric specs work too: gosu 1000:1 id resolves the IDs directly rather than looking up names in /etc/passwd.

The README's own before-and-after comparison is the clearest demonstration of why this matters. Running su -c 'exec ps aux' or sudo ps aux inside a container shows the su or sudo process holding PID 1, with ps as a child. Running the same command through gosu shows the target process itself at PID 1.

## gosu is the wrong tool outside the root-to-user container case

The README carries an explicit warning, and it is worth taking literally. The core use case is stepping down from root to a non-privileged user during container startup. Uses beyond that could suffer from vulnerabilities such as CVE-2016-2779, from which the Docker use case naturally shields the tool. The README links to issue 37 for discussion.

CVE-2016-2779 is the util-linux TTY injection issue. The point is that gosu does not try to defend against a hostile TTY, because in the container entrypoint scenario there is no interactive TTY to defend against. If you are reaching for gosu to let a user run a command as another user on a normal multi-user host, you are outside the design and outside the safety argument.

There is a second limitation that follows from the exec design. Because gosu replaces itself with the target process, there is no gosu process left to forward signals or manage a terminal. That is presented as the feature, and it is, but it also means gosu cannot do anything a supervisor does. If your entrypoint needs to start several processes, reap zombies, or restart a crashed service, gosu is not that layer. You need a supervisor, and gosu would be one step inside it.

## setpriv, chroot --userspec and su-exec compared

The README lists alternatives with the same before-and-after format, which makes the differences concrete rather than promotional.

setpriv ships in util-linux, and the README notes it is available in newer versions, >= 2.32.1-0.2 in Debian. The invocation is setpriv --reuid=nobody --regid=nogroup --init-groups ps faux. The practical difference is packaging: setpriv comes with util-linux, so it may already be present in your base image, whereas gosu is a separate binary you download and verify. If your image already has a recent util-linux, setpriv removes a download step.

chroot with --userspec provides similar behavior, as in chroot --userspec=nobody / ps aux. The catch is in the name. chroot also changes the root directory, so you have to pass / explicitly to keep the filesystem view unchanged. It is a heavier tool being asked to do a lighter job.

su-exec is the closest match in intent. The README describes it as a minimal re-write of gosu in C, much smaller as a binary, and available in the main Alpine package repository. For Alpine images that is a real advantage: one apk package instead of a download-and-verify dance. The README does flag a version constraint. Versions older than 0.3 had a severe parser bug, so you should be on 0.3 or above. The README also mentions chpst from runit, without going into detail.

The honest summary is that gosu's differentiator is not raw capability. It is the libcontainer-derived user parsing and the resulting parity with Docker's --user flag, plus a static Go binary with no libc dependency. The Dockerfile sets CGO_ENABLED 0 for all builds specifically to ensure no libc, which matters when you copy the binary into a minimal or musl-based image.

## Maintenance, release cadence and the Apache-2.0 licence

The repository is not archived, and the last push was on 2026-06-06. The most recent release is 1.19, published on 2025-09-23, preceded by 1.18 on 2025-09-05 and 1.17 on 2023-11-02. That gap between 1.17 and 1.18 is worth noticing: this is a small tool that changes when it needs to, not on a schedule. The 1.18 and 1.19 releases landed two weeks apart, which suggests a fix followed quickly by a follow-up rather than a steady feature stream.

The build is reproducible in intent. The Dockerfile pins golang:1.24.6-trixie as the build image, installs arch-test and file, and uses a fake-git shim pinned to a specific commit to synthesize version metadata. The build flags are set through BUILD_FLAGS with -trimpath and -ldflags '-d -w', and the Dockerfile comments that -s cannot be added because govulncheck would stop working, calling the roughly 0.2MiB size increase worth it. SECURITY.md is referenced at that point and is the file to read for the vulnerability-checking story.

The version number is scraped out of version.go by the build script, which then cross-grades it to semver because Go requires a full triplet. That is an unusual arrangement, and it means the value in version.go is the source of truth for what the binary reports. If you are pinning a version, check that file rather than assuming the release tag is embedded verbatim.

The licence is Apache-2.0. That is a permissive licence with an explicit patent grant and a requirement to preserve notices. Redistributing the binary inside a container image means keeping the licence and notice material with it. This is a description of the terms, not legal advice; if your organisation has a policy on bundled binaries, run it past whoever owns that policy.

## Conclusion

Adopt gosu if your entrypoint starts as root and needs to run one process as a non-privileged user with Docker's own --user semantics. Do not adopt it as a general sudo replacement, and do not expect it to stay resident to forward signals or manage a TTY. Before rolling it into an image, check the architecture string your base image reports, confirm the version.go value matches the release you intend to ship, and read SECURITY.md for the govulncheck note the Dockerfile references.

## FAQ

### What is tianon/gosu and what is it for?

It is a small Go tool that sets the user and group, then execs a command, so it does not stay in the process tree. The README says the core use case is stepping down from root to a non-privileged user during container startup, usually in the ENTRYPOINT.

### How do I install gosu?

Download the binary named for your architecture, download the matching .asc signature, fetch the maintainer's public key with gpg --recv-keys, verify the signature, and chmod +x the binary. INSTALL.md has explicit Dockerfile instructions.

### Is gosu a replacement for sudo?

No. The README states it is not intended as a sudo replacement, and warns that uses beyond stepping down from root in a container could suffer from vulnerabilities such as CVE-2016-2779.

### How does gosu compare to su-exec?

The README describes su-exec as a minimal re-write of gosu in C, giving a much smaller binary, and notes it is available in the main Alpine package repository. It also warns that su-exec versions older than 0.3 had a severe parser bug, so use 0.3 or above.

### Does gosu handle user and group names the same way Docker does?

The README says gosu uses Docker/libcontainer's own code for processing user:group, giving exact 1:1 parity with Docker's --user flag. The go.mod dependency on github.com/moby/sys/user reflects that.

## Sources

- [Issues](https://github.com/tianon/gosu/issues)
- [License: Apache-2.0](https://github.com/tianon/gosu/blob/master/LICENSE)
- [README](https://github.com/tianon/gosu/blob/master/README.md)
- [Releases](https://github.com/tianon/gosu/releases)
- [tianon/gosu on GitHub](https://github.com/tianon/gosu)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tianon-gosu
