curl: (52) Empty reply from server on a published port of an ASP.NET Core container can mean that the app inside listens only on the container's own loopback. In the case reproduced here the image set ASPNETCORE_URLS=http://localhost:8080, Kestrel logged Now listening on: http://localhost:8080, and curl on the published port got an empty reply. Make it listen on all addresses inside the container, http://+:8080.

The fix is one line in the Dockerfile, or one -e flag on docker run. The published side can stay on 127.0.0.1 on your machine; only the address inside the container changes.

The error

PS> docker run -d --name fix10-dc-web1 -p 127.0.0.1:15930:8080 fix10-dc-web:bad
982f7271a5b34e03eb9929253d3a6432a57b4adf864b3cb7f93507516347158a
...
PS> curl.exe -sS http://127.0.0.1:15930/
curl: (52) Empty reply from server

The container log looks healthy, which is what makes this one confusing:

PS> docker logs fix10-dc-web1
warn: Microsoft.AspNetCore.Hosting.Diagnostics[15]
      Overriding HTTP_PORTS '8080' and HTTPS_PORTS ''. Binding to values defined by URLS instead 'http://localhost:8080'.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:8080
...

Why it happens

The localhost in ASPNETCORE_URLS is the container's loopback, not your PC's. Inside the broken container, /proc/net/tcp listed one listening socket on port 8080 (hex 1F90), bound to 0100007F, which is 127.0.0.1; /proc/net/tcp6 showed the same for ::1. The container's own address, from docker inspect, was 172.17.0.4, and nothing listened on it:

> docker exec fix10-dc-web5 cat /proc/net/tcp /proc/net/tcp6
  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
   0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 327377 1 000000007b5eb1b8 100 0 0 10 0
  sl  local_address                         remote_address                        st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
   0: 00000000000000000000000001000000:1F90 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 327378 1 00000000d88d055e 100 0 0 10 0

> docker inspect --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" fix10-dc-web5
172.17.0.4

On the Windows side, curl.exe -v shows that the connection itself was made and the request sent; the reply was empty:

* Established connection to 127.0.0.1 (127.0.0.1 port 15930) from 127.0.0.1 port 61531
...
* Request completely sent off
...
* Empty reply from server
...
curl: (52) Empty reply from server

The log gives it away once you know where to look: the line Now listening on: http://localhost:8080, and the warning above it that URLS overrides the image's HTTP_PORTS '8080'. The aspnet image sets ASPNETCORE_HTTP_PORTS=8080 in its own environment, so the ASPNETCORE_URLS line in the Dockerfile replaced a setting that already worked. A run with that variable set to an empty value fell back to it and logged Now listening on: http://[::]:8080.

The fix

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
RUN dotnet new web -n FixLab -o . && dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "FixLab.dll"]
PS> docker run -d --name fix10-dc-web3 -p 127.0.0.1:15932:8080 fix10-dc-web:good
...
PS> docker logs fix10-dc-web3
...
      Now listening on: http://[::]:8080
...
PS> curl.exe -sS http://127.0.0.1:15932/
Hello World!

+ tells Kestrel to bind every address, and the log now says http://[::]:8080. Without rebuilding, the same result comes from overriding the variable at run time:

PS> docker run -d --name fix10-dc-web2 -p 127.0.0.1:15931:8080 -e ASPNETCORE_URLS=http://+:8080 fix10-dc-web:bad
...
PS> curl.exe -sS http://127.0.0.1:15931/
Hello World!

Keep 127.0.0.1 in the -p value if you want the port reachable only from your own PC: that address belongs to the host side of the mapping and is unrelated to the address Kestrel binds inside the container.

How it was reproduced

A two-stage Dockerfile ran dotnet new web and dotnet publish on mcr.microsoft.com/dotnet/sdk:10.0 and copied the output into mcr.microsoft.com/dotnet/aspnet:10.0 (runtime 10.0.13), with ENV ASPNETCORE_URLS=http://localhost:8080 as the planted mistake. Docker client and engine 29.9.3 on Windows, curl 8.22.0. The container and image names are the test's own. The same mistake with a different server, Ollama bound to 127.0.0.1 inside its container, printed the same curl message.

Frequently asked

Why does my ASP.NET Core container return an empty reply?
In the test Kestrel was listening on localhost, the container's own loopback: /proc/net/tcp in the container showed 127.0.0.1:8080 only, and nothing on the container's address 172.17.0.4. Set ASPNETCORE_URLS to http://+:8080 inside the container.
Do I need to publish the port on 0.0.0.0 to fix curl 52?
No. In the test the port stayed published on 127.0.0.1 and the reply came back as soon as Kestrel listened on http://+:8080 inside the container. The two addresses are on different sides of the mapping.
Which port does the aspnet image listen on by default?
The mcr.microsoft.com/dotnet/aspnet:10.0 image sets ASPNETCORE_HTTP_PORTS=8080. In the test an ASPNETCORE_URLS value overrode it, and with ASPNETCORE_URLS set to an empty value Kestrel listened on http://[::]:8080 again.

More decoded errors in the Fixes category. The Ollama version of this message is in curl: (52) Empty reply from server with OLLAMA_HOST=127.0.0.1.