The bridge between a Continuous Integration (CI) environment, such as GitHub Actions, and the isolated environment of a Docker build process is one of the most frequent points of failure for DevOps engineers. When developers attempt to pass dynamic data—such as API keys, environment-specific secrets (stage vs. prod), or versioning tags—from a GitHub workflow into a Docker image, they often encounter a fundamental architectural wall: the Docker build process does not have native access to the shell environment variables of the runner executing the action.
This disconnect occurs because the docker build command creates a separate build container. Consequently, any variable defined in the env: block of a GitHub Action YAML file is available to the GitHub Runner's shell but is invisible to the Dockerfile during the image construction phase. To overcome this, a specific mechanism involving Build Arguments (ARG) and the docker/build-push-action must be employed. Failure to correctly map these variables results in empty environment variables within the container, leading to application crashes or failed builds, particularly in frameworks like NextJS or Python-based AWS deployments.
The Mechanics of Build-Time Variable Injection
To successfully pass a value from GitHub Actions into a Docker image, one must understand the distinction between a build-time argument and a runtime environment variable. A build argument is a value passed to the Docker engine during the build process, while an environment variable is a value present when the container is actually running.
The standard technical flow for this integration is as follows:
- The secret or variable is stored in GitHub Settings (Actions -> Secrets and variables).
- The GitHub Action workflow references this secret using the
${{ secrets.VARIABLE_NAME }}syntax. - The
docker/build-push-actionpasses this value into the Docker engine via thebuild-argsparameter. - The Dockerfile declares the expected argument using the
ARGinstruction. - If the variable is needed at runtime, the Dockerfile assigns the
ARGvalue to anENVvariable.
This process ensures that sensitive data, such as an API_URL or SENTRY_AUTH_TOKEN, is injected into the image at the precise moment it is needed. For example, in a Python script intended for AWS, a developer can replace a static secret value with a dynamic one that changes based on whether the workflow is targeting a staging or production environment.
Configuration of the docker/build-push-action
The docker/build-push-action is the primary tool for automating the build and push process. However, users often make the mistake of attempting to use the env: block of the action to pass variables into the image.
As demonstrated in technical disputes regarding the action, inputs are not evaluated as environment variables within the with: block. If a user attempts to use a variable like $REGISTRY within the tags: field of the with: section, the action will return an error such as ERROR: invalid tag "$REGISTRY/github-actions-lab:test": invalid reference format. This is because GitHub Actions does not perform shell-style variable expansion on the with inputs; it expects the explicit GitHub expression syntax ${{ }}.
The correct implementation for passing multiple variables is to use the build-args parameter as a multi-line string.
yaml
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: your-dockerhub-username/your-image-name:latest
build-args: |
API_URL=${{ secrets.API_URL }}
NEXT_PUBLIC_API_KEY=${{ secrets.NEXT_PUBLIC_API_KEY }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
This configuration maps the GitHub secret directly to a Docker build argument. This is the only reliable way to ensure the value travels from the encrypted GitHub vault into the Docker build context.
Dockerfile Implementation Strategies
Declaring the variable in the YAML file is only half of the requirement. The Dockerfile must be explicitly configured to receive these values. If a build-arg is passed but the Dockerfile does not contain a corresponding ARG instruction, the value is simply ignored.
The implementation requires a two-step process within the Dockerfile to ensure the variable is available both during the build and when the container starts.
- Step 1: Define the
ARG. This allows the build process to accept the value from thebuild-push-action. - Step 2: Assign the
ARGto anENV. This persists the value so the application can access it at runtime.
Example implementation:
```dockerfile
Define the build argument
ARG SENTRYAUTHTOKEN
Assign the build argument to a permanent environment variable
ENV SENTRYAUTHTOKEN ${SENTRYAUTHTOKEN}
```
A critical architectural constraint in Docker is the "scope" of these variables. ARG and ENV instructions must be placed in the specific build stage where they are required. In a multi-stage Dockerfile (e.g., a build stage and a final production stage), an ARG declared in the first stage is not available in the second stage. To maintain the variable across the entire image lifecycle, the ARG and ENV lines must be repeated in every stage that requires access to that specific piece of data.
Security Implications of Build Arguments
The use of ARG for passing secrets introduces a significant security risk that developers must account for. Unlike secrets managed by a dedicated secret store or mounted volumes, build arguments are baked into the image's history.
The impact of this is that anyone who has access to the Docker image can run docker history and potentially see the values passed via ARG. This includes sensitive data like the SENTRY_AUTH_TOKEN or AWS secrets.
The risk profile varies based on the registry:
- Private Registry: If the image is stored in a private registry (e.g., Amazon ECR or a private GitHub Container Registry), the risk is mitigated because access to the image is restricted.
- Public Registry: If the image is pushed to a public registry (e.g., Docker Hub public repos), any user can pull the image and inspect the layers, potentially exposing the secrets.
For high-security environments, it is recommended to use build-time secrets (using --secret) rather than ARG, although ARG remains the standard for most non-critical configuration variables.
Comparison of Variable Passing Methods
The following table delineates the differences between using the env block in GitHub Actions versus using build-args in the Docker action.
| Feature | GitHub Action env: Block |
docker/build-push-action build-args |
|---|---|---|
| Scope | Available to the Runner Shell | Available to the Docker Build Engine |
| Visibility in Dockerfile | Invisible | Visible via ARG instruction |
Usage in with: block |
Not evaluated (results in error) | Fully supported via ${{ }} syntax |
| Persistence | Temporary for the job duration | Baked into image layers (if converted to ENV) |
| Primary Use Case | Configuring the runner/CLI tools | Configuring the application inside the image |
Advanced Workflow Integration and Tooling
For professional-grade deployments, the docker/build-push-action is rarely used in isolation. It is typically part of a larger ecosystem of official Docker actions designed to handle metadata and build environments.
The recommended modern pipeline involves three primary components:
docker/setup-buildx-action: This initializes the BuildKit engine, which is required for advanced features like secret mounting and efficient caching.docker/metadata-action: This automatically generates tags and labels based on the GitHub branch, pull request, or semantic versioning. This replaces the manual and error-prone process of hardcoding tags.docker/build-push-action: This performs the actual build and push, utilizing the tags generated by the metadata action and the variables passed throughbuild-args.
An example of a comprehensive production-ready workflow:
```yaml
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
name: Login to Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=ref,event=branchname: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
SENTRYAUTHTOKEN=${{ secrets.SENTRYAUTHTOKEN }}
```
Troubleshooting Common Failure Points
When variables fail to appear in the final container, the following diagnostic checklist should be applied:
- Variable Expansion Failure: Ensure you are using
${{ secrets.VARIABLE }}and not$VARIABLE. The latter refers to a shell variable on the runner, while the former is the GitHub Action expression for secrets. - Invalid Tag Errors: If you receive an "invalid reference format" error, check if you are trying to use a shell variable inside a
with:block. As noted, these are not evaluated by the action. - Stage Mismatch: Verify that the
ARGinstruction is located in the same stage as theENVinstruction. If theARGis in a "build" stage and the application runs in a "runtime" stage, the variable will be lost. - Missing ARG Declaration: Confirm that the Dockerfile actually contains the
ARGline. Without this line, thebuild-argspassed by the GitHub Action are discarded by the Docker engine. - Case Sensitivity: Docker variables are case-sensitive. Ensure that
SENTRY_AUTH_TOKENin the YAML matches theARGin the Dockerfile exactly.
Conclusion
The integration of GitHub Actions variables into Docker builds is a process of explicit hand-offs. The data must travel from the GitHub Secret store, through the build-args of the docker/build-push-action, into the ARG declaration of the Dockerfile, and finally into the ENV configuration of the image. This multi-step requirement exists because of the fundamental isolation between the host running the CI job and the container being built.
While the process is straightforward once understood, the lack of automatic environment variable inheritance often leads to frustration for users who expect the env: block of a GitHub Action to propagate into the Docker container. By utilizing the docker/setup-buildx-action and docker/metadata-action in conjunction with docker/build-push-action, developers can create a robust, scalable, and secure pipeline. However, extreme caution must be exercised when using ARG for secrets, as this method leaves a permanent footprint in the image history, necessitating the use of private registries to prevent sensitive data leakage.
Sources
- Docker Forums: How to pass GitHub Actions variable into Dockerfile
- GitHub Discussions: docker/build-push-action Environment Variables
- GitHub Issues: Game-CI Docker Environment Variables
- Release.com: How to Use GitHub Actions With Environment Variables
- GitHub Marketplace: docker-build-and-publish
- Dev.to: Environment Variables in GitHub Docker Build Push Action