GitHub Actions: build outside vs inside container?

Let’s say we’re using GitHub Actions to build and publish a container image of our app. I’m gonna pick ASP.NET Core as the app’s tech stack here, although that shouldn’t matter much.

There are two different approaches I’d like to discuss:

1. “Build outside”: build/compile app in GitHub Actions runner, copy output into container image

For example, our GitHub Actions workflow file could look like this…

name: build-outside
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout repo
      uses: actions/checkout@v2
    - name: Setup .NET Core
      uses: actions/setup-dotnet@v1
    - name: .NET Publish
      run: dotnet publish --configuration Release --nologo -p:CI=true -o $GITHUB_WORKSPACE/buildOutput src
    - name: Build and push Docker image
      uses: docker/build-push-action@v1
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
        repository: ${{ format('{0}/build-outside-test', secrets.DOCKERHUB_USERNAME) }}
        tags: latest

… and there’s a simple Dockerfile like this:

FROM mcr.microsoft.com/dotnet/core/aspnet:latest
WORKDIR /app
COPY buildOutput /app
ENTRYPOINT ["dotnet", "MyTestApp.dll"]

2. “Build inside”: build in one container, copy output to another container image

In this case, the workflow file is shorter…

name: build-inside
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout repo
      uses: actions/checkout@v2
    - name: Build and push Docker image
      uses: docker/build-push-action@v1
      with:
        dockerfile: Dockerfile_build_inside
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
        repository: ${{ format('{0}/build-inside-test', secrets.DOCKERHUB_USERNAME) }}
        tags: latest

… whereas the Dockerfile is longer, as this is now where we’re building the app itself and the final container image:

FROM mcr.microsoft.com/dotnet/core/sdk:latest AS build
WORKDIR /src
COPY src /src
RUN dotnet publish --configuration Release --nologo -p:CI=true -o ./buildOutput

FROM mcr.microsoft.com/dotnet/core/aspnet:latest AS runtime
WORKDIR /app
COPY --from=build /src/buildOutput ./
ENTRYPOINT ["dotnet", "MyTestApp.dll"]

Aside: in case you’re not familiar with multi-stage
builds
,
note the two FROM statements in that second Dockerfile. We’re
building in a first, temporary container, and then copying only the
build output into the final (runtime-optimized) container image.

Note that this second approach is explicitly recommended in the official ASP.NET Core documentation.

Trade-offs

I’ve confirmed that both approaches work and produce a working container image. Notably, build checks on pull requests “just work”™ with both approaches:

enter image description here

Now stepping away from this concrete example, here’s my current thinking on the advantages of each approach in general:

  1. Build outside:
  • Build can leverage Marketplace Actions
  • If build is complex and consists of several steps, it might be beneficial to set it up using GitHub Actions primitives – i.e. a series of jobs/tasks. That way, we can leave it to GH to optimize the build, allocate additional resources as needed, run jobs in parallel etc.
  • A little easier to inspect build failures (UI will show exactly which step failed)
  • No need to download 2nd container image during build, hence maybe saving a little bit of network bandwidth
  1. Build inside:
  • Exact, deterministic build output
  • Full control over build environment; independent of build runner
  • Container build can also run on local dev machines, producing same exact output

Questions

  1. Am I accurately describing the advantages of the two approaches?

  2. Are there any other aspects of building inside vs outside a container, specifically in GitHub Actions, which are worth mentioning?

Go to Source
Author: Max