Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 648
Max Avatar asked Jul 04 '20 22:07

Max


People also ask

Do GitHub Actions run in containers?

GitHub Actions has a relatively little known feature where you can run jobs in a container, specifically a Docker container. I liken it to delegating the entire job to the container, so every step that would run in the job will instead run in the container, including the clone/checkout of the repository.

Can GitHub Actions build Docker image?

Github actions build logs for Docker container build. You are now building Docker images automatically on Github. The logical next step is to publish your container image to a Docker registry. I suggest the Docker publish workflow for this.

Do GitHub Actions jobs run in parallel?

You can configure a GitHub Actions workflow to be triggered when an event occurs in your repository, such as a pull request being opened or an issue being created. Your workflow contains one or more jobs which can run in sequential order or in parallel.

Do GitHub Actions run as root?

/github/workspace - Note: GitHub Actions must be run by the default Docker user (root).


1 Answers

Sounds like you covered it well, I'll just pinpoint 📌 a few things.

Using multistage build is great (build inside) and it really depends on your use case. For example, if the build step is not too complicated, like your examples, then going with multistage is enough, plus it has its benefits of having a small image as an artifact.

Moving on to a complicated build - let's say you need to -

  1. Download artifact from GitHub, a release.tar.gz file and unpack it
  2. Build the unpacked file/compile it in some way
  3. Download another artifact from AWS S3
  4. You get the idea ...
  5. Eventually build your application

So here we have multiple steps, that maybe running some of them in parallel, for example, downloading artifacts, can reduce the build time. Moreover, if you split the build to Steps, you can monitor which steps failed, and send notifications accordingly.

To sum it up -

  • Having a long-multistage build in the same Dockerfile is difficult, if you find yourself writing more than 30 rows in a Dockerfile, you might consider splitting to Steps
  • Using Docker's multi-stage build is great if you want to shrink your Docker image artifact. I wouldn't use it as a pipeline
  • Splitting to Steps provides better monitoring, and saves build time due to parallelism

Let me know if you have more thoughts, it's a great topic

like image 163
Meir Gabay Avatar answered Sep 19 '22 08:09

Meir Gabay