All posts
technicaldocker

git exited with code 128 inside Docker: the HTTP/2 clone failure and the one-line fix

PreviewDrop Team·September 16, 2026·6 min read

For about a month, from roughly August 1 to September 2, 2026, every git clone inside our build image failed. Customer builds and the no-signup demo on our homepage both died at the first step with the same line:

git exited with code 128

Git's own stderr, underneath that wrapper message, said:

fatal: could not read Username for 'https://github.com': No such device or address

along with:

expected flush after ref listing

The repositories were public. Nothing asked for a username. If you landed here from a search for one of those strings, the short answer is below, and the rest of the post is how we got there and how to check it is your problem too.

RUN git config --system http.version HTTP/1.1

How it went unnoticed

We are not proud of this part. Our monitoring did not catch it. Our health checks asked whether the services were up, and they were. Few builds were being started in that window, so nobody reported it either.

What caught it was a flow test on September 2: we drove the homepage demo end to end, the way a visitor would, and it failed at the clone. That test is the lesson at the end of this post.

What git was actually doing

Exit code 128 is git's generic "fatal error" code, so it tells you nothing on its own. The useful information is in the HTTP exchange. We turned on git's tracing inside the running container and ran the cheapest command that touches the remote:

GIT_TRACE=1 GIT_TRACE_CURL=1 git ls-remote https://github.com/expressjs/express 2>&1 | less

On older git versions, GIT_CURL_VERBOSE=1 gives similar output. Look for the HTTP status lines and the request paths. What we saw:

  1. GET /expressjs/express.git/info/refs?service=git-upload-pack returned 200, over HTTP/2.
  2. The follow-up POST /expressjs/express.git/git-upload-pack returned 401.

That second response explains both error messages. A 401 tells git the server wants credentials, so git tries to prompt for a username. Inside a container there is no terminal to prompt on, which is where No such device or address comes from. expected flush after ref listing is git reporting that the exchange ended before it got the response it was waiting for. Neither message is about your credentials; both are about a request that should never have been refused.

Ruling out a block or a rate limit

A 401 from GitHub on a public repository looks like the network is being throttled or an IP is flagged. So we asked the same question with curl, from the same container, pretending to be the same git:

curl -sS -o /dev/null -w '%{http_code}\n' \
  -A 'git/2.39.5' \
  'https://github.com/expressjs/express.git/info/refs?service=git-upload-pack'

That came back 200. Same machine, same outbound address, same User-Agent. Whatever was going wrong was specific to how git itself was talking HTTP, not to who was talking.

The one-off test

Before changing the image, confirm the theory without touching any config:

git -c http.version=HTTP/1.1 ls-remote https://github.com/expressjs/express
git -c http.version=HTTP/1.1 clone --depth 1 https://github.com/expressjs/express

If those succeed where the plain commands fail, you have the same problem we had. We also tried -c protocol.version=1, which switches git's wire protocol from v2 back to v1. That worked too.

The fix, and where to put it

http.version is a normal git config key, so it can live at three levels.

System-wide, in the image. This is what we shipped. Every process in the container, whatever user it runs as, picks it up:

# git over HTTP/2 failed every clone with exit 128 (Sep 2026).
# Remove only after testing a clone without it.
RUN git config --system http.version HTTP/1.1

Per user, if you do not own the image or only one account runs git:

git config --global http.version HTTP/1.1

Per command, for a CI step or a script you do not want to change globally:

git -c http.version=HTTP/1.1 clone https://github.com/owner/repo

If you would rather keep HTTP/2 for every other host, git applies http.* settings per URL, so this narrows it to GitHub:

git config --system http.https://github.com/.version HTTP/1.1

After the image was rebuilt and rolled out, the homepage demo built one of our sample repos from submit to a live preview URL in 81 seconds.

Why HTTP/1.1 and not protocol v1

Both settings fixed it. They change different layers:

  • http.version=HTTP/1.1 changes the transport under git. Git still speaks wire protocol v2 on top of it.
  • protocol.version=1 keeps the transport and downgrades git's own protocol.

Protocol v2 is the one that lets the client tell the server which refs it wants, instead of receiving the full ref advertisement first. For shallow, single-branch clones of repos with many branches and tags, that matters. The transport version matters much less: a clone is a small number of sequential requests, and HTTP/2's multiplexing has little to multiplex. So we kept v2 and pinned the transport.

The honest trade-off is that you have pinned something, and pins outlive their reasons. Leave a comment next to it saying what broke and when, so whoever touches the Dockerfile next can test removing it rather than guessing.

What we did not prove

We pinned the protocol and the failure stopped. We did not establish the root cause, and we are not going to pretend we did. Hypotheses that fit the evidence, none confirmed:

  • A change on GitHub's side in how HTTP/2 connections are handled between the ref listing and the upload-pack request, which older clients handle differently.
  • A bug in the HTTP/2 handling of the libcurl build that git links against in our image, exposed by that kind of change.
  • Something about connection reuse between the GET and the POST that HTTP/1.1 sidesteps.

If your image is pinned to an older distribution release, the git and libcurl versions in it are pinned too. That is worth checking before you assume the problem is anywhere else. Upgrading both may remove the need for the pin; we have not tested that yet.

A smoke test that actually builds something

Once we saw the failure, fixing it took one evening. The month before that was a monitoring bug.

Every check we had asked "is the service up?" None of them asked "can the service do its job?" A build system that cannot clone is up in every sense a health endpoint understands. What would have caught this on August 1 is a scheduled test that does the whole thing: take a small public repository, submit it, wait for a URL, and fetch that URL. If any step fails, page someone. It costs one tiny build per run, and it tests the path customers actually take, including every dependency you forgot you have, like a git client talking to a code host.

If you run anything that clones repositories on a schedule, CI runners included, a canary clone of a known public repo is the cheapest insurance you can buy.

If your preview builds are failing somewhere other than the clone, the troubleshooting guide covers the common ones, and the quickstart gets a branch of your own repository live. When you are ready for every branch to get a URL, create a free account.

Want previews on your own pull requests? Start free — 2 concurrent previews, no credit card.