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:
Now stepping away from this concrete example, here's my current thinking on the advantages of each approach in general:
Questions
Am I accurately describing the advantages of the two approaches?
Are there any other aspects of building inside vs outside a container, specifically in GitHub Actions, which are worth mentioning?
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.
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.
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.
/github/workspace - Note: GitHub Actions must be run by the default Docker user (root).
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 -
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 -
Let me know if you have more thoughts, it's a great topic
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With