Docker Build and Publish
Why Docker?
Many organizations require obtaining and deploying software packages from an image for ease of deployment. Furthermore, there can be strict requirements for those images to be signed or provide provenance attestations, as well as come from a trusted source such as Docker Hub or GitHub Container Registry.
Publishing to Docker Hub and verifying provenance allows FINOS projects to increase adoption by making deployments easy, consistent and trusted - something especially important for enterprise users.
Getting started
In order to start publishing your image to Docker Hub, you'll first need to create a Dockerfile to define what the runtime environment should look like, install dependencies and build the project.
Then, you'll need a GitHub workflow .github/workflows/docker-publish.yml to check out the repository, and then build and publish the Docker image.
Optionally, a docker-compose.yml can be created for ease of local development and testing. This is not needed when publishing to Docker Hub, which only requires a Dockerfile.
Dockerfile
Your Dockerfile will vary wildly depending on which dependencies you need to build the project, your project's runtime environment(s), etc. A guide on how to write a basic Dockerfile is available in the Docker documentation.
Even if there isn't a one-size-fits-all solution, there are general best practices that are good to follow when writing a Dockerfile.
Here's a sample Dockerfile from GitProxy:
FROM node:24@sha256:5a593d74b632d1c6f816457477b6819760e13624455d587eef0fa418c8d0777b AS builder
USER root
WORKDIR /out
COPY package*.json ./
COPY tsconfig.json tsconfig.publish.json proxy.config.json config.schema.json test-e2e.proxy.config.json vite.config.ts index.html index.ts ./
RUN npm pkg delete scripts.prepare && npm ci --include=dev
COPY src/ /out/src/
COPY public/ /out/public/
RUN npm run build-ui \
&& npx tsc --project tsconfig.publish.json \
&& cp config.schema.json dist/ \
&& npm prune --omit=dev
FROM node:24@sha256:5a593d74b632d1c6f816457477b6819760e13624455d587eef0fa418c8d0777b AS production
COPY --from=builder /out/package*.json ./
COPY --from=builder /out/node_modules/ /app/node_modules/
COPY --from=builder /out/dist/ /app/dist/
COPY --from=builder /out/build /app/dist/build/
COPY proxy.config.json config.schema.json ./
COPY docker-entrypoint.sh /docker-entrypoint.sh
USER root
RUN apt-get update && apt-get install -y \
git tini \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app/.data /app/.tmp /app/.remote \
&& chown -R 1000:1000 /app
USER 1000
WORKDIR /app
EXPOSE 8080 8000
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["node", "--enable-source-maps", "dist/index.js"]
This file is specific to GitProxy, but it showcases elements that are good to have in any Dockerfile:
- Multi-stage builds: We divide the work into a
builderstage that compiles/installs everything, and aproductionstage that copies over the final artifacts. This keeps image sizes small and prevents shipping dev tooling into production - Pinning images to SHA digests: Notice that image versions include a SHA. This is needed because tags are mutable. Pinning to a specific SHA guarantees the environment is properly replicated
- Running as non-root: Setting
USER 1000allows minimizing privileges before initializing the app - Tini entrypoint: Sets the entrypoint to Tini, which allows reaping zombie processes and forwarding signals from Docker to the app
docker-publish.yml
This file should be created in your .github/workflows directory to automate the build and publish process. In this file, we can detail when and how we want to publish to Docker Hub, for example:
- Publish a
my-project:maintag every time something gets pushed tomain - Publish a
my-project:latesttag whenever a new version of our software is published - Publish a
my-project:X.Ytag when publishing a specific version of our software
Here's an example of a docker-publish.yml workflow from GitProxy:
name: Build and Publish Docker Image
on:
push:
branches: [main]
release:
types: [published]
jobs:
docker-build-publish:
name: Build and Publish Docker Image
runs-on: ubuntu-latest
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Docker Hub
if: github.repository_owner == 'finos'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
with:
username: finos
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set Docker Image Tag
id: tags
run: |
if [ "${{ github.event_name }}" = "release" ]; then
echo "tags=${{ github.repository }}:${{ github.ref_name }},${{ github.repository }}:latest" >> $GITHUB_OUTPUT
else
echo "tags=${{ github.repository }}:main" >> $GITHUB_OUTPUT
fi
- name: Build and Publish Docker Image
if: github.repository_owner == 'finos'
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
with:
context: .
file: Dockerfile
push: true
tags: ${{ steps.tags.outputs.tags }}
provenance: true
You can tweak when to run the workflow as follows:
on:
push:
branches: [main] # Run when pushing to main
release:
types: [published] # Run when publishing a release (via GitHub)
Note that the following section requires a DOCKER_PASSWORD repository secret to log into the finos Docker Hub account. Please contact help@finos.org to set it up:
- name: Log in to Docker Hub
if: github.repository_owner == 'finos' # Only allow workflow to run from upstream repository
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
with:
username: finos
password: ${{ secrets.DOCKER_PASSWORD }}
The following bit tags the image depending on whether the workflow got triggered on release or a regular push. Then, it automatically sets the tag name to the repository name and appends :latest, :main or :X.Y depending on what triggered the flow:
- name: Set Docker Image Tag
id: tags
run: |
if [ "${{ github.event_name }}" = "release" ]; then
echo "tags=${{ github.repository }}:${{ github.ref_name }},${{ github.repository }}:latest" >> $GITHUB_OUTPUT
else
echo "tags=${{ github.repository }}:main" >> $GITHUB_OUTPUT
fi
Finally, the image gets published to Docker Hub using the tags determined earlier. The provenance: true flag includes a provenance attestation, often used for security and auditing purposes:
- name: Build and Publish Docker Image
if: github.repository_owner == 'finos'
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
with:
context: .
file: Dockerfile
push: true
tags: ${{ steps.tags.outputs.tags }}
provenance: true
docker-compose.yml
A docker-compose.yml can be optionally used for using images locally and testing. This isn't required for deploying to Docker Hub.
Here's an example docker-compose.yml from GitProxy for reference.
Verification
If everything is working as expected, you should find your published image in the Docker Hub FINOS profile after successfully running the docker-publish.yml workflow.
If it doesn't show up, there was likely an error in the workflow itself, or during the Dockerfile build process. For more details on why the flow failed, check out the Actions tab on your repository and look for the Build and Publish Docker Image action to see the workflow execution output along with the reason for failure. You may also want to verify that the Dockerfile build works locally before running it in your project's CI pipeline.