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.