Search code examples
dockerasp.net-core-webapigoogle-cloud-rungoogle-cloud-build

Continuously deploy ASP.NET WebAPI from GitHub to Google Cloud Run


I need some help getting this to work. I've got it to the point where my container is built, but Cloud Run says the service isn't listening on the specified port.

Updated: 6/28/2023

  • Changed project to not use top-level statements, in order to have a Main method for entrypoint.
  • Added cloudbuild.yaml to repo root (posted below)
  • Updated Dockerfile to correct multiple issues (updated below)
    • Biggest problems were the copy commands and then specifying port to run on
  • Latest status:
    • Builds successfully in Cloud Build, with default (for ASP.NET 6.0 WebAPI) output type of "Console Application"
    • Cloud Run still failing to start, but the logs reveal better details (posted below)

Details:

Source code for my sample app, a simple ASP.NET 6.0 WebAPI with default WeatherForecast controller, can be found here: https://github.com/andre-engelbrecht/sample_net_api

cloudbuild.yaml:

steps:
  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'build'
      - '-t'
      - 'gcr.io/$PROJECT_ID/sample-api'
      - '-f'
      - 'Sample_API/Sample_API.API/Dockerfile'  # Update the path to your Dockerfile
      - '.'  # Set the build context to the root directory
images:
  - 'gcr.io/$PROJECT_ID/sample-api'

Dockerfile: (which builds successfully on CloudBuild)

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["Sample_API/Sample_API.API/Sample_API.API.csproj", "Sample_API.API/"]
COPY ["Sample_API/Sample_API.API/", "Sample_API.API/"]
RUN dotnet restore "Sample_API.API/Sample_API.API.csproj"
WORKDIR "/src/Sample_API.API"
RUN ls -a
RUN dotnet build "Sample_API.API.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "Sample_API.API.csproj" -c Release -o /app/publish /p:UseAppHost=true

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS final
#Set the environment variable to listen on the specified port
ENV ASPNETCORE_URLS=http://*:$PORT  
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Sample_API.API.dll"]

Cloud Run complains of the following:

The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable

cloud run failed

Cloud Run Logs:

- A fatal error was encountered. The library 'libhostpolicy.so' required to execute the application was not found in '/app/'.
- Failed to run as a self-contained app.
- The application was run as a self-contained app because '/app/Sample_API.API.runtimeconfig.json' was not found.
- If this should be a framework-dependent app, add the '/app/Sample_API.API.runtimeconfig.json' file and specify the appropriate framework.

Question:

What can I do to get this to work? I'm new to Docker and GCP, so I really don't know what I'm doing wrong? And though there are many guides out there, I could not find one for my particular use case.

According to this post, the problem is that my service is not listening on the defined port (8080), but how/where do I specify what port it should listen on when in Cloud Run? Locally I've set it up so that it starts on port 8080, but that didn't fix GCP.

I followed the following guides to get this far:

Any help will be appreciated!


Solution

  • I finally got it all working!


    Core things I needed to add / change:

    1. Create (or alter) project without top-level statements

    Initially I had (unknowingly) created my project with top-level statements, this meant that there was no "Main" method in Program.cs to use as an entry point.

    2. Include cloudbuild.yaml to define build and deploy steps

    There's likely more then one way to do this, but I ended up adding a couldbuild.yaml file to the root of my repo to define build and deploy steps for Cloud Build.

    This is what my cloudbuild.yaml file looks like:
    steps:
      - name: 'gcr.io/cloud-builders/docker'
        args:
          - 'build'
          - '-t'
          - 'gcr.io/$PROJECT_ID/sample-api'
          - '-f'
          - 'Sample_API/Sample_API.API/Dockerfile'  # Update the path to your Dockerfile
          - '.'  # Set the build context to the root directory
    
      - name: 'gcr.io/cloud-builders/docker'
        args: ['push', 'gcr.io/$PROJECT_ID/sample-api']
    
      - name: 'gcr.io/cloud-builders/gcloud'
        args:
          - 'run'
          - 'deploy'
          - 'sample-net-api'  # Update with your service name
          - '--image=gcr.io/$PROJECT_ID/sample-api'
          - '--region=us-central1'
          - '--platform=managed'
          # Additional deployment options go here
    
    images:
      - 'gcr.io/$PROJECT_ID/sample-api'
    

    3. Fix my Dockerfile

    My Dockerfile has some errors, most notably the commands to copy the project- and other files to the /src directory during build.

    I also added a 2nd project to my solution, a class-library to define core code and keep my controller lean. Which mandated further updates to make the COPY more generic.

    This is what my Dockerfile looks like
    FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
    WORKDIR /src
    # Copy all files to /src directory
    COPY ["Sample_API/", "Sample_API/"] 
    # "Navigate" to main project directory
    WORKDIR "/src/Sample_API/Sample_API.API"
    # Restore and build
    RUN dotnet restore "Sample_API.API.csproj"
    RUN dotnet build "Sample_API.API.csproj" -c Release -o /app/build
    
    FROM build AS publish
    # Publish
    RUN dotnet publish "Sample_API.API.csproj" -c Release -o /app/publish /p:UseAppHost=true
    
    FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS final
    # Set the environment variable to listen on the specified port
    # Only used in load Docker test deployments - not used in Cloud Run
    # ENV ASPNETCORE_URLS=http://*:$PORT  
    WORKDIR /app
    COPY --from=publish /app/publish .
    ENTRYPOINT ["dotnet", "Sample_API.API.dll"]
    

    4. Start service on port defined by Cloud Run

    Lastly I had to ensure my service started on the port that was specified in Cloud Run setup, to avoid the infamous The user-provided container failed to start and listen on the port defined provided by the PORT=$PORT environment variable error.

    I did this by adding the following to Program.cs:

    if (Environment.GetEnvironmentVariable("PORT") != null)
    {
        builder.WebHost.UseUrls($"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT")}");
    }
    

    A big shoutout to ChatGPT, which when you ask the right questions, can indeed be very useful.