2
votes

Trying to upload a docker image with dotnet core webapi project.

A requirement of cloud run is that it is listening on port 8080.

I believe I am doing that but when I create a cloud-run service after pushing to container registry GCP comes back with:

"Container failed to start. Failed to start and then listen on the port defined by the PORT environment variable. Logs for this revision might contain more information."

Locally I have kestrel listening on 8080. Also have had the container listing on 8080. But when I pushed either one I get the failure to start message...? Any suggestions or attempts at doing this?

@wlhee Here is the LOG from cloud run:

2019-04-13T05:24:53.462592ZHosting environment: Production
2019-04-13T05:24:53.462657ZContent root path: /app
2019-04-13T05:24:53.462678ZNow listening on: http://[::]:80
2019-04-13T05:24:53.462697ZApplication started. Press Ctrl+C to shut down.
2019-04-13T05:28:48.973934834ZContainer terminated by the container manager on signal 9.

"Container failed to start. Failed to start and then listen on the port defined by the PORT environment variable. Logs for this revision might contain more information."

~ DOCKER FILE

FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base
WORKDIR /app
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS build
WORKDIR /src
COPY ["simplecore.csproj", "simplecore/"]
RUN dotnet restore "simplecore/simplecore.csproj"
COPY . .
WORKDIR "/src/simplecore"
RUN dotnet build "simplecore.csproj" -c Release -o /app

FROM build AS publish
RUN dotnet publish "simplecore.csproj" -c Release -o /app

FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "simplecore.dll"]
~ HERE IS MY MAIN FROM CORE APP

public static void Main(string[] args)
        {
            //CreateWebHostBuilder(args).Build().Run();

            var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            //.UseIISIntegration()
            .UseStartup<Startup>()
            .UseUrls("http://0.0.0.0:8080/")
            .Build();

            host.Run();

        }
2
Do you see any logs printed in "logging"?wlhee
@wlhee Here is the LOG from cloud run: 2019-04-13T05:24:53.462592ZHosting environment: Production 2019-04-13T05:24:53.462657ZContent root path: /app 2019-04-13T05:24:53.462678ZNow listening on: http://[::]:80 2019-04-13T05:24:53.462697ZApplication started. Press Ctrl+C to shut down. 2019-04-13T05:28:48.973934834ZContainer terminated by the container manager on signal 9. sorry about the formattinguser3693978
it looks like the app was listening on port 80 rather than 8080?wlhee
Even when I try to make the app listen on port 8080 it failsuser3693978
Please follow these instructions and confirm your container runs locally.Steren

2 Answers

1
votes

The following solution worked for me:

In the Dockerfile modify the lines

FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "simplecore.dll"]

by adding ENV:

FROM base AS final
ENV ASPNETCORE_URLS=http://*:${PORT}
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "simplecore.dll"]

Add a health controller to listen on the root route:

[Route("/")]
[ApiController]
public class HealthController : ControllerBase
{

    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return Ok();
    }
}

In Program.cs configure Kestrel to listen on the PORT environment variable:

    public static IWebHostBuilder CreateWebHostBuilder(string[] args)
    {
        var port = Environment.GetEnvironmentVariable("PORT");

        return WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .UseKestrel()
            .ConfigureKestrel((context, options) =>
            {
                options.Listen(IPAddress.IPv6Any, Convert.ToInt32(port));
            });
    }

Lastly add a default route in Startup.cs:

app.UseMvc(routes => 
{
    routes.MapRoute("default", "{controller=Health}/{action=Get}");
});

Rebuild and deploy

1
votes

IJB's answer is spot on and worked for us, but in our case we were using ASP.NET Core 3.0, so we had to modify Program.cs slightly like so:

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>()
                        .ConfigureKestrel(options =>
                        {
                            var port = Convert.ToInt32(Environment.GetEnvironmentVariable("PORT") ?? "80");
                            options.Listen(IPAddress.Any, port);
                        });
                });
    }

We did not need to call app.UseMvc(...). We did need to add a HealthController with just one GET method pointing at route "/" as shown in IJB's answer, reiterated below.

[Route("/")]
    [ApiController]
    public class HealthController : ControllerBase
    {
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return Ok();
        }
    }

Also, and this had us stumped for a while, if you're going to deploy to Cloud Run, the gcloud beta run deploy does NOT push the Docker image but reuses the one already deployed. That had us super confused until we realized that the Docker image it was trying to deploy had an old image ID. So to deploy to Container Registry and then deploy to Cloud Run, you'd want to do the following:

  1. Build the Docker image:
docker image build -t my-web-api -t gcr.io/<your project ID here>/my-web-api -f Dockerfile .

You can rename "my-web-api" above to whatever you want.

  1. Push the Docker image (before you do this, make sure you install gcloud tools and configure Docker by typing in gcloud auth login, gcloud config set project <your project ID here>, gcloud auth configure-docker):
docker push gcr.io/<your project ID here>/my-web-api:latest

Replace "my-web-api" above with what you used in step #1.

  1. Deploy to Cloud Run:
gcloud beta run deploy --image gcr.io/<your project ID here>/my-web-api --region us-central1

You need the "region" param because as of this writing Cloud Run is only available in us-central1.

In order to get our .NET Core 3.0 project to build and run properly, we also had to modify the Dockerfile. It took us quite a while to figure this out, so hopefully we saved you some time here. Use it as a reference and compare it with the Dockerfile visual studio generated for you, adding the relevant pieces. Here is our Dockerfile in its entirety:

FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim AS base

# uncomment if you need to deploy a service account JSON file to access Google Cloud resources
#ARG GOOGLE_APPLICATION_CREDENTIALS_FILE
ARG ASPNETCORE_ENVIRONMENT

# uncomment if you need to deploy a service account JSON file
#ENV GOOGLE_APPLICATION_CREDENTIALS="/app/$GOOGLE_APPLICATION_CREDENTIALS_FILE"
ENV ASPNETCORE_ENVIRONMENT=$ASPNETCORE_ENVIRONMENT

# uncomment if you need to deploy a service account JSON file
#COPY "keys/$GOOGLE_APPLICATION_CREDENTIALS_FILE" "/app/"

WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS build
WORKDIR /src
COPY ["<your project name>.csproj", "./"]
RUN dotnet restore "<your project name>.csproj"
COPY . .
WORKDIR "/src/<your project name>"
RUN dotnet build "<your project name>" -c Release -o /app

FROM build AS publish
RUN dotnet publish "<your project name>" -c Release -o /app --self-contained --runtime linux-x64

FROM base AS final
ENV ASPNETCORE_URLS=http://*:${PORT}
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "<your DLL file here>.dll"]

We had to connect to Google Cloud Storage from our container so we also "injected" a service account JSON file that we had stored in a ./keys/ folder (if you do this, don't forget to add that folder to .gitignore or equivalent). Our build server would inject the right file based on the environment, like so:

docker image build -t my-web-api -t gcr.io/<project ID here>/my-web-api -f <project dir>/Dockerfile --build-arg GOOGLE_APPLICATION_CREDENTIALS_FILE="my-service-acccount.json" --build-arg ASPNETCORE_ENVIRONMENT="Development" .

You can follow the same pattern to inject other environment variables, too. Anyway, hope this helps you solve the perplexing "Container failed to start" error.