|
| 1 | +--- |
| 2 | +id: docker |
| 3 | +title: Docker Build and Publish |
| 4 | +--- |
| 5 | + |
| 6 | +## Why Docker? |
| 7 | + |
| 8 | +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. |
| 9 | + |
| 10 | +[Publishing to Docker Hub](https://hub.docker.com/r/finos/) and verifying provenance allows FINOS projects to increase adoption by making deployments easy, consistent and trusted - something especially important for enterprise users. |
| 11 | + |
| 12 | +## Getting started |
| 13 | + |
| 14 | +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. |
| 15 | + |
| 16 | +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. |
| 17 | + |
| 18 | +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. |
| 19 | + |
| 20 | +### `Dockerfile` |
| 21 | + |
| 22 | +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](https://docs.docker.com/get-started/docker-concepts/building-images/writing-a-dockerfile/) is available in the Docker documentation. |
| 23 | + |
| 24 | +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. |
| 25 | + |
| 26 | +Here's a [sample Dockerfile from GitProxy](https://github.com/finos/git-proxy/blob/main/Dockerfile): |
| 27 | + |
| 28 | +```Dockerfile |
| 29 | +FROM node:24@sha256:5a593d74b632d1c6f816457477b6819760e13624455d587eef0fa418c8d0777b AS builder |
| 30 | + |
| 31 | +USER root |
| 32 | + |
| 33 | +WORKDIR /out |
| 34 | + |
| 35 | +COPY package*.json ./ |
| 36 | +COPY tsconfig.json tsconfig.publish.json proxy.config.json config.schema.json test-e2e.proxy.config.json vite.config.ts index.html index.ts ./ |
| 37 | + |
| 38 | +RUN npm pkg delete scripts.prepare && npm ci --include=dev |
| 39 | + |
| 40 | +COPY src/ /out/src/ |
| 41 | +COPY public/ /out/public/ |
| 42 | + |
| 43 | +RUN npm run build-ui \ |
| 44 | + && npx tsc --project tsconfig.publish.json \ |
| 45 | + && cp config.schema.json dist/ \ |
| 46 | + && npm prune --omit=dev |
| 47 | + |
| 48 | +FROM node:24@sha256:5a593d74b632d1c6f816457477b6819760e13624455d587eef0fa418c8d0777b AS production |
| 49 | + |
| 50 | +COPY --from=builder /out/package*.json ./ |
| 51 | +COPY --from=builder /out/node_modules/ /app/node_modules/ |
| 52 | +COPY --from=builder /out/dist/ /app/dist/ |
| 53 | +COPY --from=builder /out/build /app/dist/build/ |
| 54 | +COPY proxy.config.json config.schema.json ./ |
| 55 | +COPY docker-entrypoint.sh /docker-entrypoint.sh |
| 56 | + |
| 57 | +USER root |
| 58 | + |
| 59 | +RUN apt-get update && apt-get install -y \ |
| 60 | + git tini \ |
| 61 | + && rm -rf /var/lib/apt/lists/* |
| 62 | + |
| 63 | +RUN mkdir -p /app/.data /app/.tmp /app/.remote \ |
| 64 | + && chown -R 1000:1000 /app |
| 65 | + |
| 66 | +USER 1000 |
| 67 | + |
| 68 | +WORKDIR /app |
| 69 | + |
| 70 | +EXPOSE 8080 8000 |
| 71 | + |
| 72 | +ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] |
| 73 | +CMD ["node", "--enable-source-maps", "dist/index.js"] |
| 74 | +``` |
| 75 | + |
| 76 | +This file is specific to GitProxy, but it showcases elements that are good to have in any `Dockerfile`: |
| 77 | + |
| 78 | +- **Multi-stage builds**: We divide the work into a `builder` stage that compiles/installs everything, and a `production` stage that copies over the final artifacts. This keeps image sizes small and prevents shipping dev tooling into production |
| 79 | +- **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 |
| 80 | +- **Running as non-root**: Setting `USER 1000` allows minimizing privileges before initializing the app |
| 81 | +- **Tini entrypoint**: Sets the entrypoint to Tini, which allows reaping zombie processes and forwarding signals from Docker to the app |
| 82 | + |
| 83 | +### `docker-publish.yml` |
| 84 | + |
| 85 | +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: |
| 86 | + |
| 87 | +- Publish a `my-project:main` tag every time something gets pushed to `main` |
| 88 | +- Publish a `my-project:latest` tag whenever a new version of our software is published |
| 89 | +- Publish a `my-project:X.Y` tag when publishing a specific version of our software |
| 90 | + |
| 91 | +Here's an example of a [`docker-publish.yml` workflow from GitProxy](https://github.com/finos/git-proxy/blob/main/.github/workflows/docker-publish.yml): |
| 92 | + |
| 93 | +```yml |
| 94 | +name: Build and Publish Docker Image |
| 95 | + |
| 96 | +on: |
| 97 | + push: |
| 98 | + branches: [main] |
| 99 | + release: |
| 100 | + types: [published] |
| 101 | + |
| 102 | +jobs: |
| 103 | + docker-build-publish: |
| 104 | + name: Build and Publish Docker Image |
| 105 | + runs-on: ubuntu-latest |
| 106 | + |
| 107 | + steps: |
| 108 | + - name: Set up Docker Buildx |
| 109 | + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 |
| 110 | + |
| 111 | + - name: Checkout Repository |
| 112 | + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 |
| 113 | + |
| 114 | + - name: Log in to Docker Hub |
| 115 | + if: github.repository_owner == 'finos' |
| 116 | + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 |
| 117 | + with: |
| 118 | + username: finos |
| 119 | + password: ${{ secrets.DOCKER_PASSWORD }} |
| 120 | + |
| 121 | + - name: Set Docker Image Tag |
| 122 | + id: tags |
| 123 | + run: | |
| 124 | + if [ "${{ github.event_name }}" = "release" ]; then |
| 125 | + echo "tags=${{ github.repository }}:${{ github.ref_name }},${{ github.repository }}:latest" >> $GITHUB_OUTPUT |
| 126 | + else |
| 127 | + echo "tags=${{ github.repository }}:main" >> $GITHUB_OUTPUT |
| 128 | + fi |
| 129 | +
|
| 130 | + - name: Build and Publish Docker Image |
| 131 | + if: github.repository_owner == 'finos' |
| 132 | + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 |
| 133 | + with: |
| 134 | + context: . |
| 135 | + file: Dockerfile |
| 136 | + push: true |
| 137 | + tags: ${{ steps.tags.outputs.tags }} |
| 138 | + provenance: true |
| 139 | +``` |
| 140 | +
|
| 141 | +You can tweak when to run the workflow as follows: |
| 142 | +
|
| 143 | +```yml |
| 144 | +on: |
| 145 | + push: |
| 146 | + branches: [main] # Run when pushing to main |
| 147 | + release: |
| 148 | + types: [published] # Run when publishing a release (via GitHub) |
| 149 | +``` |
| 150 | +
|
| 151 | +Note that the following section requires a `DOCKER_PASSWORD` repository secret to log into the `finos` Docker Hub account. Please contact [help@finos.org](mailto:help@finos.org) to set it up: |
| 152 | + |
| 153 | +```yml |
| 154 | + - name: Log in to Docker Hub |
| 155 | + if: github.repository_owner == 'finos' # Only allow workflow to run from upstream repository |
| 156 | + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 |
| 157 | + with: |
| 158 | + username: finos |
| 159 | + password: ${{ secrets.DOCKER_PASSWORD }} |
| 160 | +``` |
| 161 | + |
| 162 | +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: |
| 163 | + |
| 164 | +```yml |
| 165 | + - name: Set Docker Image Tag |
| 166 | + id: tags |
| 167 | + run: | |
| 168 | + if [ "${{ github.event_name }}" = "release" ]; then |
| 169 | + echo "tags=${{ github.repository }}:${{ github.ref_name }},${{ github.repository }}:latest" >> $GITHUB_OUTPUT |
| 170 | + else |
| 171 | + echo "tags=${{ github.repository }}:main" >> $GITHUB_OUTPUT |
| 172 | + fi |
| 173 | +``` |
| 174 | + |
| 175 | +Finally, the image gets published to Docker Hub using the tags determined earlier. The `provenance: true` flag includes a [provenance attestation](https://docs.docker.com/build/metadata/attestations/slsa-provenance/), often used for security and auditing purposes: |
| 176 | + |
| 177 | +```yml |
| 178 | + - name: Build and Publish Docker Image |
| 179 | + if: github.repository_owner == 'finos' |
| 180 | + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 |
| 181 | + with: |
| 182 | + context: . |
| 183 | + file: Dockerfile |
| 184 | + push: true |
| 185 | + tags: ${{ steps.tags.outputs.tags }} |
| 186 | + provenance: true |
| 187 | +``` |
| 188 | + |
| 189 | +### docker-compose.yml |
| 190 | + |
| 191 | +A `docker-compose.yml` can be optionally used for using images locally and testing. This isn't required for deploying to Docker Hub. |
| 192 | + |
| 193 | +Here's an [example `docker-compose.yml` from GitProxy](https://github.com/finos/git-proxy/blob/main/docker-compose.yml) for reference. |
| 194 | + |
| 195 | +## Verification |
| 196 | + |
| 197 | +If everything is working as expected, you should find your published image in the [Docker Hub FINOS profile](https://hub.docker.com/r/finos/) after successfully running the `docker-publish.yml` workflow. |
| 198 | + |
| 199 | +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. |
0 commit comments