├── .gitignore ├── .gitattributes ├── img └── Graph.png ├── .dockerignore ├── rootfs └── prometheus │ └── etc │ └── default.yml ├── .github └── workflows │ ├── tags.yml │ ├── readme.yml │ ├── version.yml │ ├── cve.yml │ ├── cron.update.yml │ └── docker.yml ├── .json ├── LICENSE ├── compose.yaml ├── project.md ├── arch.dockerfile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # default 2 | maintain/ 3 | node_modules/ -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # default 2 | * text=auto 3 | *.sh eol=lf -------------------------------------------------------------------------------- /img/Graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11notes/docker-prometheus/HEAD/img/Graph.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # default 2 | .git* 3 | maintain/ 4 | LICENSE 5 | *.md 6 | img/ 7 | node_modules/ -------------------------------------------------------------------------------- /rootfs/prometheus/etc/default.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | 4 | scrape_configs: 5 | - job_name: "prometheus" 6 | static_configs: 7 | - targets: ["localhost:3000"] -------------------------------------------------------------------------------- /.github/workflows/tags.yml: -------------------------------------------------------------------------------- 1 | name: tags 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | docker: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: build docker image 11 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 12 | with: 13 | workflow: docker.yml 14 | wait-for-completion: false 15 | token: "${{ secrets.REPOSITORY_TOKEN }}" 16 | inputs: '{ "release":"true", "readme":"true" }' -------------------------------------------------------------------------------- /.github/workflows/readme.yml: -------------------------------------------------------------------------------- 1 | name: readme 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | readme: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: update README.md 11 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 12 | with: 13 | wait-for-completion: false 14 | workflow: docker.yml 15 | token: "${{ secrets.REPOSITORY_TOKEN }}" 16 | inputs: '{ "build":"false", "release":"false", "readme":"true", "platform":"amd64" }' -------------------------------------------------------------------------------- /.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "11notes/prometheus", 3 | "name": "prometheus", 4 | "root": "/prometheus", 5 | "semver": { 6 | "version": "3.8.1" 7 | }, 8 | "readme": { 9 | "description": "Run prometheus rootless and distroless.", 10 | "introduction": "Prometheus, a Cloud Native Computing Foundation project, is a systems and service monitoring system. It collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts when specified conditions are observed.", 11 | "built": { 12 | "prometheus": "https://github.com/prometheus/prometheus" 13 | }, 14 | "distroless": { 15 | "layers": [ 16 | "11notes/distroless", 17 | "11notes/distroless:curl" 18 | ] 19 | }, 20 | "comparison": { 21 | "image": "prom/prometheus" 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 11notes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: version 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'set semver for build' 7 | type: string 8 | required: true 9 | jobs: 10 | docker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: init / base64 nested json 14 | uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 15 | with: 16 | script: | 17 | const { Buffer } = require('node:buffer'); 18 | const etc = { 19 | version:"${{ github.event.inputs.version }}", 20 | semver:{disable:{rolling: true}} 21 | }; 22 | core.exportVariable('WORKFLOW_BASE64JSON', Buffer.from(JSON.stringify(etc)).toString('base64')); 23 | 24 | - name: build docker image 25 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 26 | with: 27 | wait-for-completion: false 28 | workflow: docker.yml 29 | token: "${{ secrets.REPOSITORY_TOKEN }}" 30 | inputs: '{ "release":"false", "readme":"false", "etc":"${{ env.WORKFLOW_BASE64JSON }}" }' -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | name: "monitoring" 2 | services: 3 | prometheus: 4 | depends_on: 5 | adguard: 6 | condition: "service_healthy" 7 | restart: true 8 | image: "11notes/prometheus:3.8.1" 9 | read_only: true 10 | environment: 11 | TZ: "Europe/Zurich" 12 | PROMETHEUS_CONFIG: |- 13 | global: 14 | scrape_interval: 1s 15 | 16 | scrape_configs: 17 | - job_name: "dnspyre" 18 | static_configs: 19 | - targets: ["dnspyre:3000"] 20 | volumes: 21 | - "prometheus.etc:/prometheus/etc" 22 | - "prometheus.var:/prometheus/var" 23 | ports: 24 | - "3000:3000/tcp" 25 | networks: 26 | frontend: 27 | restart: "always" 28 | 29 | # this image will execute 100k (10 x 10000) queries against adguard to fill your Prometheus with some data 30 | dnspyre: 31 | depends_on: 32 | prometheus: 33 | condition: "service_healthy" 34 | restart: true 35 | image: "11notes/distroless:dnspyre" 36 | command: "--server adguard -c 10 -n 3 -t A --prometheus ':3000' https://raw.githubusercontent.com/11notes/static/refs/heads/main/src/benchmarks/dns/fqdn/10000" 37 | read_only: true 38 | environment: 39 | TZ: "Europe/Zurich" 40 | networks: 41 | frontend: 42 | 43 | adguard: 44 | image: "11notes/adguard:0.107.64" 45 | read_only: true 46 | environment: 47 | TZ: "Europe/Zurich" 48 | volumes: 49 | - "adguard.etc:/adguard/etc" 50 | - "adguard.var:/adguard/var" 51 | tmpfs: 52 | # tmpfs volume because of read_only: true 53 | - "/adguard/run:uid=1000,gid=1000" 54 | ports: 55 | - "53:53/udp" 56 | - "53:53/tcp" 57 | - "3010:3000/tcp" 58 | networks: 59 | frontend: 60 | sysctls: 61 | # allow rootless container to access ports < 1024 62 | net.ipv4.ip_unprivileged_port_start: 53 63 | restart: "always" 64 | 65 | volumes: 66 | prometheus.etc: 67 | prometheus.var: 68 | adguard.etc: 69 | adguard.var: 70 | 71 | networks: 72 | frontend: -------------------------------------------------------------------------------- /project.md: -------------------------------------------------------------------------------- 1 | ${{ image: Graph.png }} 2 | 3 | ${{ content_synopsis }} This image will run Prometheus [rootless](https://github.com/11notes/RTFM/blob/main/linux/container/image/rootless.md) and [distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md), for maximum security and performance. You can either provide your own config file or configure Prometheus directly inline in your compose. If you run the compose example, you can open the following [URL](http://localhost:3000/query?g0.expr=histogram_quantile%280.9%2C+sum+by+%28le%29+%28rate%28dnspyre_dns_requests_duration_seconds_bucket%5B1m%5D%29%29%29&g0.show_tree=0&g0.tab=graph&g0.range_input=1m&g0.res_type=auto&g0.res_density=medium&g0.display_mode=lines&g0.show_exemplars=0) to see the statistics of your DNS benchmark just like in the screenshot. 4 | 5 | ${{ content_uvp }} Good question! Because ... 6 | 7 | ${{ github:> [!IMPORTANT] }} 8 | ${{ github:> }}* ... this image runs [rootless](https://github.com/11notes/RTFM/blob/main/linux/container/image/rootless.md) as 1000:1000 9 | ${{ github:> }}* ... this image has no shell since it is [distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md) 10 | ${{ github:> }}* ... this image is auto updated to the latest version via CI/CD 11 | ${{ github:> }}* ... this image has a health check 12 | ${{ github:> }}* ... this image runs read-only 13 | ${{ github:> }}* ... this image is automatically scanned for CVEs before and after publishing 14 | ${{ github:> }}* ... this image is created via a secure and pinned CI/CD process 15 | ${{ github:> }}* ... this image is very small 16 | 17 | If you value security, simplicity and optimizations to the extreme, then this image might be for you. 18 | 19 | ${{ content_comparison }} 20 | 21 | ${{ title_config }} 22 | ```yaml 23 | ${{ include: ./rootfs/prometheus/etc/default.yml }} 24 | ``` 25 | 26 | ${{ title_volumes }} 27 | * **${{ json_root }}/etc** - Directory of your config 28 | * **${{ json_root }}/var** - Directory of all dynamic data and database 29 | 30 | ${{ content_compose }} 31 | 32 | ${{ content_defaults }} 33 | 34 | ${{ content_environment }} 35 | | `PROMETHEUS_CONFIG` | If not using a yml file you can provide your config as inline yml directly in your compose | | 36 | 37 | ${{ content_source }} 38 | 39 | ${{ content_parent }} 40 | 41 | ${{ content_built }} 42 | 43 | ${{ content_tips }} -------------------------------------------------------------------------------- /.github/workflows/cve.yml: -------------------------------------------------------------------------------- 1 | name: cve 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "30 15 */2 * *" 7 | 8 | jobs: 9 | cve: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: init / checkout 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 14 | with: 15 | ref: ${{ github.ref_name }} 16 | fetch-depth: 0 17 | 18 | - name: init / setup environment 19 | uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 20 | with: 21 | script: | 22 | const { existsSync, readFileSync } = require('node:fs'); 23 | const { resolve } = require('node:path'); 24 | const { inspect } = require('node:util'); 25 | const { Buffer } = require('node:buffer'); 26 | const inputs = `${{ toJSON(github.event.inputs) }}`; 27 | const opt = {input:{}, dot:{}}; 28 | 29 | try{ 30 | if(inputs.length > 0){ 31 | opt.input = JSON.parse(inputs); 32 | if(opt.input?.etc){ 33 | opt.input.etc = JSON.parse(Buffer.from(opt.input.etc, 'base64').toString('ascii')); 34 | } 35 | } 36 | }catch(e){ 37 | core.warning('could not parse github.event.inputs'); 38 | } 39 | 40 | try{ 41 | const path = resolve('.json'); 42 | if(existsSync(path)){ 43 | try{ 44 | opt.dot = JSON.parse(readFileSync(path).toString()); 45 | }catch(e){ 46 | throw new Error('could not parse .json'); 47 | } 48 | }else{ 49 | throw new Error('.json does not exist'); 50 | } 51 | }catch(e){ 52 | core.setFailed(e); 53 | } 54 | 55 | core.info(inspect(opt, {showHidden:false, depth:null, colors:true})); 56 | 57 | core.exportVariable('WORKFLOW_IMAGE', `${opt.dot.image}:${(opt.dot?.semver?.version === undefined) ? 'rolling' : opt.dot.semver.version}`); 58 | core.exportVariable('WORKFLOW_GRYPE_SEVERITY_CUTOFF', (opt.dot?.grype?.severity || 'high')); 59 | 60 | 61 | - name: grype / scan 62 | id: grype 63 | uses: anchore/scan-action@dc6246fcaf83ae86fcc6010b9824c30d7320729e 64 | with: 65 | image: ${{ env.WORKFLOW_IMAGE }} 66 | fail-build: true 67 | severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} 68 | output-format: 'sarif' 69 | by-cve: true 70 | cache-db: true -------------------------------------------------------------------------------- /arch.dockerfile: -------------------------------------------------------------------------------- 1 | # ╔═════════════════════════════════════════════════════╗ 2 | # ║ SETUP ║ 3 | # ╚═════════════════════════════════════════════════════╝ 4 | # GLOBAL 5 | ARG APP_UID=1000 \ 6 | APP_GID=1000 \ 7 | BUILD_ROOT=/go/prometheus \ 8 | BUILD_SRC=prometheus/prometheus.git 9 | ARG BUILD_BIN=${BUILD_ROOT}/prometheus 10 | 11 | # :: FOREIGN IMAGES 12 | FROM 11notes/distroless AS distroless 13 | FROM 11notes/distroless:localhealth AS distroless-localhealth 14 | FROM 11notes/util AS util 15 | 16 | # ╔═════════════════════════════════════════════════════╗ 17 | # ║ BUILD ║ 18 | # ╚═════════════════════════════════════════════════════╝ 19 | # :: PROMETHEUS 20 | FROM 11notes/go:1.24 AS build 21 | ARG APP_VERSION \ 22 | BUILD_SRC \ 23 | BUILD_ROOT \ 24 | BUILD_BIN 25 | 26 | RUN set -ex; \ 27 | apk --update --no-cache add \ 28 | g++ \ 29 | make \ 30 | bash \ 31 | nodejs \ 32 | npm; 33 | 34 | RUN set -ex; \ 35 | eleven git clone ${BUILD_SRC} v${APP_VERSION}; 36 | 37 | RUN set -ex; \ 38 | cd ${BUILD_ROOT}; \ 39 | make build; 40 | 41 | RUN set -ex; \ 42 | eleven distroless ${BUILD_BIN}; 43 | 44 | # :: ENTRYPOINT 45 | FROM 11notes/go:1.24 AS entrypoint 46 | COPY ./build / 47 | 48 | RUN set -ex; \ 49 | cd /go/entrypoint; \ 50 | eleven go build entrypoint main.go; \ 51 | eleven distroless entrypoint; 52 | 53 | 54 | # :: FILE SYSTEM 55 | FROM alpine AS file-system 56 | COPY --from=util / / 57 | ARG APP_ROOT 58 | USER root 59 | 60 | RUN set -ex; \ 61 | eleven mkdir /distroless${APP_ROOT}/{etc,var}; 62 | 63 | 64 | # ╔═════════════════════════════════════════════════════╗ 65 | # ║ IMAGE ║ 66 | # ╚═════════════════════════════════════════════════════╝ 67 | # :: HEADER 68 | FROM scratch 69 | 70 | # :: default arguments 71 | ARG TARGETPLATFORM \ 72 | TARGETOS \ 73 | TARGETARCH \ 74 | TARGETVARIANT \ 75 | APP_IMAGE \ 76 | APP_NAME \ 77 | APP_VERSION \ 78 | APP_ROOT \ 79 | APP_UID \ 80 | APP_GID \ 81 | APP_NO_CACHE 82 | 83 | # :: default environment 84 | ENV APP_IMAGE=${APP_IMAGE} \ 85 | APP_NAME=${APP_NAME} \ 86 | APP_VERSION=${APP_VERSION} \ 87 | APP_ROOT=${APP_ROOT} 88 | 89 | # :: multi-stage 90 | COPY --from=distroless / / 91 | COPY --from=distroless-localhealth / / 92 | COPY --from=build /distroless/ / 93 | COPY --from=entrypoint /distroless/ / 94 | COPY --from=file-system --chown=${APP_UID}:${APP_GID} /distroless/ / 95 | COPY --chown=${APP_UID}:${APP_GID} ./rootfs/ / 96 | 97 | # :: PERSISTENT DATA 98 | VOLUME ["${APP_ROOT}/etc", "${APP_ROOT}/var"] 99 | 100 | # :: MONITORING 101 | HEALTHCHECK --interval=5s --timeout=2s --start-period=5s \ 102 | CMD ["/usr/local/bin/localhealth", "http://127.0.0.1:3000/"] 103 | 104 | # :: EXECUTE 105 | USER ${APP_UID}:${APP_GID} 106 | ENTRYPOINT ["/usr/local/bin/entrypoint"] -------------------------------------------------------------------------------- /.github/workflows/cron.update.yml: -------------------------------------------------------------------------------- 1 | name: cron-update 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 5 * * *" 7 | 8 | jobs: 9 | cron-update: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | actions: read 14 | contents: write 15 | 16 | steps: 17 | - name: init / checkout 18 | uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 19 | with: 20 | ref: 'master' 21 | fetch-depth: 0 22 | 23 | - name: cron-update / get latest version 24 | run: | 25 | echo "LATEST_VERSION=$(curl -s https://api.github.com/repos/prometheus/prometheus/releases/latest | jq -r '.tag_name' | sed 's/release-//' | sed 's/v//')" >> "${GITHUB_ENV}" 26 | echo "LATEST_TAG=$(git describe --abbrev=0 --tags `git rev-list --tags --max-count=1` | sed 's/v//')" >> "${GITHUB_ENV}" 27 | 28 | - name: cron-update / setup node 29 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 30 | with: 31 | node-version: '20' 32 | - run: npm i semver 33 | 34 | - name: cron-update / compare latest with current version 35 | uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 36 | with: 37 | script: | 38 | const { existsSync, readFileSync, writeFileSync } = require('node:fs'); 39 | const { resolve } = require('node:path'); 40 | const { inspect } = require('node:util'); 41 | const semver = require('semver') 42 | const repository = {dot:{}}; 43 | 44 | try{ 45 | const path = resolve('.json'); 46 | if(existsSync(path)){ 47 | try{ 48 | repository.dot = JSON.parse(readFileSync(path).toString()); 49 | }catch(e){ 50 | throw new Error('could not parse .json'); 51 | } 52 | }else{ 53 | throw new Error('.json does not exist'); 54 | } 55 | }catch(e){ 56 | core.setFailed(e); 57 | } 58 | 59 | const latest = semver.valid(semver.coerce('${{ env.LATEST_VERSION }}')); 60 | const current = semver.valid(semver.coerce(repository.dot.semver.version)); 61 | const tag = semver.valid(semver.coerce('${{ env.LATEST_TAG }}')); 62 | 63 | if(latest && latest !== current){ 64 | core.info(`new ${semver.diff(current, latest)} release found (${latest})!`) 65 | repository.dot.semver.version = latest; 66 | if(tag){ 67 | core.exportVariable('WORKFLOW_NEW_TAG', semver.inc(tag, semver.diff(current, latest))); 68 | } 69 | 70 | if(repository.dot.semver?.latest){ 71 | repository.dot.semver.latest = repository.dot.semver.version; 72 | } 73 | 74 | if(repository.dot?.readme?.comparison?.image){ 75 | repository.dot.readme.comparison.image = repository.dot.readme.comparison.image.replace(current, repository.dot.semver.version); 76 | } 77 | 78 | try{ 79 | writeFileSync(resolve('.json'), JSON.stringify(repository.dot, null, 2)); 80 | core.exportVariable('WORKFLOW_AUTO_UPDATE', true); 81 | 82 | }catch(e){ 83 | core.setFailed(e); 84 | } 85 | }else{ 86 | core.info('no new release found'); 87 | } 88 | 89 | core.info(inspect(repository.dot, {showHidden:false, depth:null, colors:true})); 90 | 91 | - name: cron-update / checkout 92 | id: checkout 93 | if: env.WORKFLOW_AUTO_UPDATE == 'true' 94 | run: | 95 | git config user.name "github-actions[bot]" 96 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 97 | git add .json 98 | git commit -m "chore: upgrade to ${{ env.LATEST_VERSION }}" 99 | git push origin HEAD:master 100 | 101 | - name: cron-update / tag 102 | if: env.WORKFLOW_AUTO_UPDATE == 'true' && steps.checkout.outcome == 'success' 103 | run: | 104 | SHA256=$(git rev-list --branches --max-count=1) 105 | git tag -a v${{ env.WORKFLOW_NEW_TAG }} -m "v${{ env.WORKFLOW_NEW_TAG }}" ${SHA256} 106 | git push --follow-tags 107 | 108 | - name: cron-update / build docker image 109 | if: env.WORKFLOW_AUTO_UPDATE == 'true' && steps.checkout.outcome == 'success' 110 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 111 | with: 112 | workflow: docker.yml 113 | wait-for-completion: false 114 | token: "${{ secrets.REPOSITORY_TOKEN }}" 115 | inputs: '{ "release":"true", "readme":"true" }' 116 | ref: "v${{ env.WORKFLOW_NEW_TAG }}" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![banner](https://raw.githubusercontent.com/11notes/static/refs/heads/main/img/banner/README.png) 2 | 3 | # PROMETHEUS 4 | ![size](https://img.shields.io/badge/image_size-${{ image_size }}-green?color=%2338ad2d)![5px](https://raw.githubusercontent.com/11notes/static/refs/heads/main/img/markdown/transparent5x2px.png)![pulls](https://img.shields.io/docker/pulls/11notes/prometheus?color=2b75d6)![5px](https://raw.githubusercontent.com/11notes/static/refs/heads/main/img/markdown/transparent5x2px.png)[](https://github.com/11notes/docker-prometheus/issues)![5px](https://raw.githubusercontent.com/11notes/static/refs/heads/main/img/markdown/transparent5x2px.png)![swiss_made](https://img.shields.io/badge/Swiss_Made-FFFFFF?labelColor=FF0000&logo=data:image/svg%2bxml;base64,PHN2ZyB2ZXJzaW9uPSIxIiB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDMyIDMyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0idHJhbnNwYXJlbnQiLz4KICA8cGF0aCBkPSJtMTMgNmg2djdoN3Y2aC03djdoLTZ2LTdoLTd2LTZoN3oiIGZpbGw9IiNmZmYiLz4KPC9zdmc+) 5 | 6 | Run prometheus rootless and distroless. 7 | 8 | # INTRODUCTION 📢 9 | 10 | Prometheus, a Cloud Native Computing Foundation project, is a systems and service monitoring system. It collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts when specified conditions are observed. 11 | 12 | ![GRAPH](https://github.com/11notes/docker-prometheus/blob/master/img/Graph.png?raw=true) 13 | 14 | # SYNOPSIS 📖 15 | **What can I do with this?** This image will run Prometheus [rootless](https://github.com/11notes/RTFM/blob/main/linux/container/image/rootless.md) and [distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md), for maximum security and performance. You can either provide your own config file or configure Prometheus directly inline in your compose. If you run the compose example, you can open the following [URL](http://localhost:3000/query?g0.expr=histogram_quantile%280.9%2C+sum+by+%28le%29+%28rate%28dnspyre_dns_requests_duration_seconds_bucket%5B1m%5D%29%29%29&g0.show_tree=0&g0.tab=graph&g0.range_input=1m&g0.res_type=auto&g0.res_density=medium&g0.display_mode=lines&g0.show_exemplars=0) to see the statistics of your DNS benchmark just like in the screenshot. 16 | 17 | # UNIQUE VALUE PROPOSITION 💶 18 | **Why should I run this image and not the other image(s) that already exist?** Good question! Because ... 19 | 20 | > [!IMPORTANT] 21 | >* ... this image runs [rootless](https://github.com/11notes/RTFM/blob/main/linux/container/image/rootless.md) as 1000:1000 22 | >* ... this image has no shell since it is [distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md) 23 | >* ... this image is auto updated to the latest version via CI/CD 24 | >* ... this image has a health check 25 | >* ... this image runs read-only 26 | >* ... this image is automatically scanned for CVEs before and after publishing 27 | >* ... this image is created via a secure and pinned CI/CD process 28 | >* ... this image is very small 29 | 30 | If you value security, simplicity and optimizations to the extreme, then this image might be for you. 31 | 32 | # COMPARISON 🏁 33 | Below you find a comparison between this image and the most used or original one. 34 | 35 | | **image** | **size on disk** | **init default as** | **[distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md)** | supported architectures 36 | | ---: | ---: | :---: | :---: | :---: | 37 | | prom/prometheus | 378MB | 65534:65534 | ❌ | amd64, arm64, armv7, ppc64le, s390x | 38 | 39 | # DEFAULT CONFIG 📑 40 | ```yaml 41 | global: 42 | scrape_interval: 10s 43 | 44 | scrape_configs: 45 | - job_name: "prometheus" 46 | static_configs: 47 | - targets: ["localhost:3000"] 48 | ``` 49 | 50 | # VOLUMES 📁 51 | * **/prometheus/etc** - Directory of your config 52 | * **/prometheus/var** - Directory of all dynamic data and database 53 | 54 | # COMPOSE ✂️ 55 | ```yaml 56 | name: "monitoring" 57 | services: 58 | prometheus: 59 | depends_on: 60 | adguard: 61 | condition: "service_healthy" 62 | restart: true 63 | image: "11notes/prometheus:3.8.1" 64 | read_only: true 65 | environment: 66 | TZ: "Europe/Zurich" 67 | PROMETHEUS_CONFIG: |- 68 | global: 69 | scrape_interval: 1s 70 | 71 | scrape_configs: 72 | - job_name: "dnspyre" 73 | static_configs: 74 | - targets: ["dnspyre:3000"] 75 | volumes: 76 | - "prometheus.etc:/prometheus/etc" 77 | - "prometheus.var:/prometheus/var" 78 | ports: 79 | - "3000:3000/tcp" 80 | networks: 81 | frontend: 82 | restart: "always" 83 | 84 | # this image will execute 100k (10 x 10000) queries against adguard to fill your Prometheus with some data 85 | dnspyre: 86 | depends_on: 87 | prometheus: 88 | condition: "service_healthy" 89 | restart: true 90 | image: "11notes/distroless:dnspyre" 91 | command: "--server adguard -c 10 -n 3 -t A --prometheus ':3000' https://raw.githubusercontent.com/11notes/static/refs/heads/main/src/benchmarks/dns/fqdn/10000" 92 | read_only: true 93 | environment: 94 | TZ: "Europe/Zurich" 95 | networks: 96 | frontend: 97 | 98 | adguard: 99 | image: "11notes/adguard:0.107.64" 100 | read_only: true 101 | environment: 102 | TZ: "Europe/Zurich" 103 | volumes: 104 | - "adguard.etc:/adguard/etc" 105 | - "adguard.var:/adguard/var" 106 | tmpfs: 107 | # tmpfs volume because of read_only: true 108 | - "/adguard/run:uid=1000,gid=1000" 109 | ports: 110 | - "53:53/udp" 111 | - "53:53/tcp" 112 | - "3010:3000/tcp" 113 | networks: 114 | frontend: 115 | sysctls: 116 | # allow rootless container to access ports < 1024 117 | net.ipv4.ip_unprivileged_port_start: 53 118 | restart: "always" 119 | 120 | volumes: 121 | prometheus.etc: 122 | prometheus.var: 123 | adguard.etc: 124 | adguard.var: 125 | 126 | networks: 127 | frontend: 128 | ``` 129 | To find out how you can change the default UID/GID of this container image, consult the [RTFM](https://github.com/11notes/RTFM/blob/main/linux/container/image/11notes/how-to.changeUIDGID.md#change-uidgid-the-correct-way). 130 | 131 | # DEFAULT SETTINGS 🗃️ 132 | | Parameter | Value | Description | 133 | | --- | --- | --- | 134 | | `user` | docker | user name | 135 | | `uid` | 1000 | [user identifier](https://en.wikipedia.org/wiki/User_identifier) | 136 | | `gid` | 1000 | [group identifier](https://en.wikipedia.org/wiki/Group_identifier) | 137 | | `home` | /prometheus | home directory of user docker | 138 | 139 | # ENVIRONMENT 📝 140 | | Parameter | Value | Default | 141 | | --- | --- | --- | 142 | | `TZ` | [Time Zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | | 143 | | `DEBUG` | Will activate debug option for container image and app (if available) | | 144 | | `PROMETHEUS_CONFIG` | If not using a yml file you can provide your config as inline yml directly in your compose | | 145 | 146 | # MAIN TAGS 🏷️ 147 | These are the main tags for the image. There is also a tag for each commit and its shorthand sha256 value. 148 | 149 | * [3.8.1](https://hub.docker.com/r/11notes/prometheus/tags?name=3.8.1) 150 | 151 | ### There is no latest tag, what am I supposed to do about updates? 152 | It is my opinion that the ```:latest``` tag is a bad habbit and should not be used at all. Many developers introduce **breaking changes** in new releases. This would messed up everything for people who use ```:latest```. If you don’t want to change the tag to the latest [semver](https://semver.org/), simply use the short versions of [semver](https://semver.org/). Instead of using ```:3.8.1``` you can use ```:3``` or ```:3.8```. Since on each new version these tags are updated to the latest version of the software, using them is identical to using ```:latest``` but at least fixed to a major or minor version. Which in theory should not introduce breaking changes. 153 | 154 | If you still insist on having the bleeding edge release of this app, simply use the ```:rolling``` tag, but be warned! You will get the latest version of the app instantly, regardless of breaking changes or security issues or what so ever. You do this at your own risk! 155 | 156 | # REGISTRIES ☁️ 157 | ``` 158 | docker pull 11notes/prometheus:3.8.1 159 | docker pull ghcr.io/11notes/prometheus:3.8.1 160 | docker pull quay.io/11notes/prometheus:3.8.1 161 | ``` 162 | 163 | # SOURCE 💾 164 | * [11notes/prometheus](https://github.com/11notes/docker-prometheus) 165 | 166 | # PARENT IMAGE 🏛️ 167 | > [!IMPORTANT] 168 | >This image is not based on another image but uses [scratch](https://hub.docker.com/_/scratch) as the starting layer. 169 | >The image consists of the following distroless layers that were added: 170 | >* [11notes/distroless](https://github.com/11notes/docker-distroless/blob/master/arch.dockerfile) - contains users, timezones and Root CA certificates, nothing else 171 | >* [11notes/distroless:curl](https://github.com/11notes/docker-distroless/blob/master/curl.dockerfile) - app to execute HTTP requests 172 | 173 | # BUILT WITH 🧰 174 | * [prometheus](https://github.com/prometheus/prometheus) 175 | 176 | # GENERAL TIPS 📌 177 | > [!TIP] 178 | >* Use a reverse proxy like Traefik, Nginx, HAproxy to terminate TLS and to protect your endpoints 179 | >* Use Let’s Encrypt DNS-01 challenge to obtain valid SSL certificates for your services 180 | 181 | # ElevenNotes™️ 182 | This image is provided to you at your own risk. Always make backups before updating an image to a different version. Check the [releases](https://github.com/11notes/docker-prometheus/releases) for breaking changes. If you have any problems with using this image simply raise an [issue](https://github.com/11notes/docker-prometheus/issues), thanks. If you have a question or inputs please create a new [discussion](https://github.com/11notes/docker-prometheus/discussions) instead of an issue. You can find all my other repositories on [github](https://github.com/11notes?tab=repositories). 183 | 184 | *created 17.12.2025, 06:24:54 (CET)* -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | run-name: ${{ inputs.run-name }} 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | run-name: 8 | description: 'set run-name for workflow (multiple calls)' 9 | type: string 10 | required: false 11 | default: 'docker' 12 | 13 | platform: 14 | description: 'list of platforms to build for' 15 | type: string 16 | required: false 17 | default: "amd64,arm64,arm/v7" 18 | 19 | build: 20 | description: 'set WORKFLOW_BUILD' 21 | required: false 22 | default: 'true' 23 | 24 | release: 25 | description: 'set WORKFLOW_GITHUB_RELEASE' 26 | required: false 27 | default: 'false' 28 | 29 | readme: 30 | description: 'set WORKFLOW_GITHUB_README' 31 | required: false 32 | default: 'false' 33 | 34 | etc: 35 | description: 'base64 encoded json string' 36 | required: false 37 | 38 | jobs: 39 | # ╔═════════════════════════════════════════════════════╗ 40 | # ║ ║ 41 | # ║ ║ 42 | # ║ CREATE PLATFORM MATRIX ║ 43 | # ║ ║ 44 | # ║ ║ 45 | # ╚═════════════════════════════════════════════════════╝ 46 | matrix: 47 | name: create job matrix 48 | runs-on: ubuntu-latest 49 | outputs: 50 | stringify: ${{ steps.setup-matrix.outputs.stringify }} 51 | 52 | steps: 53 | - name: matrix / setup list 54 | id: setup-matrix 55 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 56 | with: 57 | script: | 58 | const platforms = "${{ github.event.inputs.platform }}".split(","); 59 | const matrix = {include:[]}; 60 | for(const platform of platforms){ 61 | switch(platform){ 62 | case "amd64": matrix.include.push({platform:platform, runner:"ubuntu-24.04"}); break; 63 | case "arm64": matrix.include.push({platform:platform, runner:"ubuntu-24.04-arm"}); break; 64 | case "arm/v7": matrix.include.push({platform:platform, runner:"ubuntu-24.04-arm"}); break; 65 | } 66 | } 67 | const stringify = JSON.stringify(matrix); 68 | core.setOutput('stringify', stringify); 69 | core.info(stringify); 70 | 71 | 72 | # ╔═════════════════════════════════════════════════════╗ 73 | # ║ ║ 74 | # ║ ║ 75 | # ║ BUILD CONTAINER IMAGE ║ 76 | # ║ ║ 77 | # ║ ║ 78 | # ╚═════════════════════════════════════════════════════╝ 79 | docker: 80 | name: create container image 81 | runs-on: ${{ matrix.runner }} 82 | strategy: 83 | fail-fast: false 84 | matrix: ${{ fromJSON(needs.matrix.outputs.stringify) }} 85 | outputs: 86 | DOCKER_IMAGE_NAME: ${{ steps.setup-environment.outputs.DOCKER_IMAGE_NAME }} 87 | DOCKER_IMAGE_MERGE_TAGS: ${{ steps.setup-environment.outputs.DOCKER_IMAGE_MERGE_TAGS }} 88 | WORKFLOW_BUILD: ${{ steps.setup-environment.outputs.WORKFLOW_BUILD }} 89 | 90 | timeout-minutes: 1440 91 | 92 | services: 93 | registry: 94 | image: registry:2 95 | ports: 96 | - 5000:5000 97 | 98 | permissions: 99 | actions: read 100 | contents: write 101 | packages: write 102 | attestations: write 103 | id-token: write 104 | security-events: write 105 | 106 | needs: matrix 107 | 108 | steps: 109 | # ╔═════════════════════════════════════════════════════╗ 110 | # ║ SETUP ENVIRONMENT ║ 111 | # ╚═════════════════════════════════════════════════════╝ 112 | # CHECKOUT ALL DEPTHS (ALL TAGS) 113 | - name: init / checkout 114 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 115 | with: 116 | ref: ${{ github.ref_name }} 117 | fetch-depth: 0 118 | 119 | # SETUP ENVIRONMENT VARIABLES AND INPUTS 120 | - name: init / setup environment 121 | id: setup-environment 122 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 123 | with: 124 | script: | 125 | const { existsSync, readFileSync } = require('node:fs'); 126 | const { resolve } = require('node:path'); 127 | const { inspect } = require('node:util'); 128 | const { Buffer } = require('node:buffer'); 129 | const inputs = `${{ toJSON(github.event.inputs) }}`. 130 | replace(/"platform":\s*"\[(.+)\]",/i, `"platform": [$1],`); 131 | const opt = {input:{}, dot:{}}; 132 | 133 | try{ 134 | if(inputs.length > 0){ 135 | opt.input = JSON.parse(inputs); 136 | if(opt.input?.etc){ 137 | opt.input.etc = JSON.parse(Buffer.from(opt.input.etc, 'base64').toString('ascii')); 138 | } 139 | } 140 | }catch(e){ 141 | core.warning('could not parse github.event.inputs'); 142 | core.warning(inputs); 143 | } 144 | 145 | try{ 146 | const path = resolve('.json'); 147 | if(existsSync(path)){ 148 | try{ 149 | opt.dot = JSON.parse(readFileSync(path).toString()); 150 | }catch(e){ 151 | throw new Error('could not parse .json'); 152 | } 153 | }else{ 154 | throw new Error('.json does not exist'); 155 | } 156 | }catch(e){ 157 | core.setFailed(e); 158 | } 159 | 160 | core.info(inspect(opt, {showHidden:false, depth:null, colors:true})); 161 | 162 | const docker = { 163 | image:{ 164 | name:opt.dot.image, 165 | arch:(opt.input?.etc?.arch || opt.dot?.arch || 'linux/amd64,linux/arm64'), 166 | prefix:((opt.input?.etc?.semverprefix) ? `${opt.input?.etc?.semverprefix}-` : ''), 167 | suffix:((opt.input?.etc?.semversuffix) ? `-${opt.input?.etc?.semversuffix}` : ''), 168 | description:(opt.dot?.readme?.description || ''), 169 | platform:{ 170 | sanitized:"${{ matrix.platform }}".replace(/[^A-Z-a-z0-9]+/i, ""), 171 | }, 172 | tags:[], 173 | }, 174 | app:{ 175 | image:opt.dot.image, 176 | name:opt.dot.name, 177 | version:(opt.input?.etc?.version || opt.dot?.semver?.version), 178 | root:opt.dot.root, 179 | UID:(opt.input?.etc?.uid || 1000), 180 | GID:(opt.input?.etc?.gid || 1000), 181 | no_cache:new Date().getTime(), 182 | }, 183 | cache:{ 184 | registry:'localhost:5000/', 185 | }, 186 | tags:[], 187 | merge_tags:[], 188 | }; 189 | 190 | docker.cache.name = `${docker.image.name}:${docker.image.prefix}buildcache${docker.image.suffix}`; 191 | docker.cache.grype = `${docker.cache.registry}${docker.image.name}:${docker.image.prefix}grype${docker.image.suffix}`; 192 | docker.app.prefix = docker.image.prefix; 193 | docker.app.suffix = docker.image.suffix; 194 | 195 | // setup tags 196 | if(!opt.dot?.semver?.disable?.rolling && !opt.input.etc?.semver?.disable?.rolling){ 197 | docker.image.tags.push('rolling'); 198 | } 199 | if(opt.input?.etc?.dockerfile !== 'arch.dockerfile' && opt.input?.etc?.tag){ 200 | docker.image.tags.push(opt.input.etc.tag); 201 | docker.image.tags.push(`${opt.input.etc.tag}-${docker.app.version}`); 202 | docker.cache.name = `${docker.image.name}:buildcache-${opt.input.etc.tag}`; 203 | }else if(docker.app.version !== 'latest'){ 204 | const semver = docker.app.version.split('.'); 205 | if(Array.isArray(semver)){ 206 | if(semver.length >= 1) docker.image.tags.push(`${semver[0]}`); 207 | if(semver.length >= 2) docker.image.tags.push(`${semver[0]}.${semver[1]}`); 208 | if(semver.length >= 3) docker.image.tags.push(`${semver[0]}.${semver[1]}.${semver[2]}`); 209 | } 210 | if(opt.dot?.semver?.stable && new RegExp(opt.dot?.semver.stable, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('stable'); 211 | if(opt.dot?.semver?.latest && new RegExp(opt.dot?.semver.latest, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('latest'); 212 | }else{ 213 | docker.image.tags.push('latest'); 214 | } 215 | 216 | for(const tag of docker.image.tags){ 217 | docker.tags.push(`${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}-${docker.image.platform.sanitized}`); 218 | docker.tags.push(`ghcr.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}-${docker.image.platform.sanitized}`); 219 | docker.tags.push(`quay.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}-${docker.image.platform.sanitized}`); 220 | docker.merge_tags.push(`${docker.image.prefix}${tag}${docker.image.suffix}`); 221 | } 222 | 223 | // setup build arguments 224 | if(opt.input?.etc?.build?.args){ 225 | for(const arg in opt.input.etc.build.args){ 226 | docker.app[arg] = opt.input.etc.build.args[arg]; 227 | } 228 | } 229 | if(opt.dot?.build?.args){ 230 | for(const arg in opt.dot.build.args){ 231 | docker.app[arg] = opt.dot.build.args[arg]; 232 | } 233 | } 234 | const arguments = []; 235 | for(const argument in docker.app){ 236 | arguments.push(`APP_${argument.toUpperCase()}=${docker.app[argument]}`); 237 | } 238 | 239 | // export to environment 240 | core.exportVariable('DOCKER_CACHE_REGISTRY', docker.cache.registry); 241 | core.exportVariable('DOCKER_CACHE_NAME', `${docker.cache.name}-${docker.image.platform.sanitized}`); 242 | core.exportVariable('DOCKER_CACHE_GRYPE', docker.cache.grype); 243 | 244 | core.exportVariable('DOCKER_IMAGE_NAME', docker.image.name); 245 | core.setOutput('DOCKER_IMAGE_NAME', docker.image.name); 246 | core.exportVariable('DOCKER_IMAGE_TAGS', docker.tags.join(',')); 247 | core.exportVariable('DOCKER_IMAGE_MERGE_TAGS', docker.merge_tags.join("\r\n")); 248 | core.setOutput('DOCKER_IMAGE_MERGE_TAGS', docker.merge_tags.join("\r\n")); 249 | core.exportVariable('DOCKER_IMAGE_DESCRIPTION', docker.image.description); 250 | core.exportVariable('DOCKER_IMAGE_ARGUMENTS', arguments.join("\r\n")); 251 | core.exportVariable('DOCKER_IMAGE_DOCKERFILE', opt.input?.etc?.dockerfile || 'arch.dockerfile'); 252 | core.exportVariable('DOCKER_IMAGE_PLATFORM_SANITIZED', docker.image.platform.sanitized); 253 | core.exportVariable('DOCKER_IMAGE_NAME_AND_VERSION', `${docker.image.name}:${docker.app.version}`); 254 | 255 | core.exportVariable('WORKFLOW_BUILD', (opt.input?.build === undefined) ? false : opt.input.build); 256 | core.setOutput('WORKFLOW_BUILD', (opt.input?.build === undefined) ? false : opt.input.build); 257 | core.exportVariable('WORKFLOW_CREATE_RELEASE', (opt.input?.release === undefined) ? false : opt.input.release); 258 | core.exportVariable('WORKFLOW_CREATE_README', (opt.input?.readme === undefined) ? false : opt.input.readme); 259 | core.exportVariable('WORKFLOW_GRYPE_FAIL_ON_SEVERITY', (opt.dot?.grype?.fail === undefined) ? true : opt.dot.grype.fail); 260 | core.exportVariable('WORKFLOW_GRYPE_SEVERITY_CUTOFF', (opt.dot?.grype?.severity || 'high')); 261 | 262 | 263 | # ╔═════════════════════════════════════════════════════╗ 264 | # ║ CONTAINER REGISTRY LOGIN ║ 265 | # ╚═════════════════════════════════════════════════════╝ 266 | # DOCKER HUB 267 | - name: docker / login to hub 268 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 269 | with: 270 | username: 11notes 271 | password: ${{ secrets.DOCKER_TOKEN }} 272 | 273 | # GITHUB CONTAINER REGISTRY 274 | - name: github / login to ghcr 275 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 276 | with: 277 | registry: ghcr.io 278 | username: 11notes 279 | password: ${{ secrets.GITHUB_TOKEN }} 280 | 281 | # REDHAT QUAY 282 | - name: quay / login to quay 283 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 284 | with: 285 | registry: quay.io 286 | username: 11notes+github 287 | password: ${{ secrets.QUAY_TOKEN }} 288 | 289 | 290 | # ╔═════════════════════════════════════════════════════╗ 291 | # ║ BUILD CONTAINER IMAGE ║ 292 | # ╚═════════════════════════════════════════════════════╝ 293 | # SETUP QEMU 294 | - name: container image / setup qemu 295 | if: env.WORKFLOW_BUILD == 'true' 296 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 297 | with: 298 | image: tonistiigi/binfmt:qemu-v8.1.5 299 | cache-image: false 300 | 301 | # SETUP BUILDX BUILDER WITH USING LOCAL REGISTRY 302 | - name: container image / setup buildx 303 | if: env.WORKFLOW_BUILD == 'true' 304 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 305 | with: 306 | driver-opts: network=host 307 | 308 | # BUILD CONTAINER IMAGE FROM GLOBAL CACHE (DOCKER HUB) AND PUSH TO LOCAL CACHE 309 | - name: container image / build 310 | if: env.WORKFLOW_BUILD == 'true' 311 | id: image-build 312 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 313 | with: 314 | context: . 315 | file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} 316 | push: true 317 | platforms: linux/${{ matrix.platform }} 318 | cache-from: type=registry,ref=${{ env.DOCKER_CACHE_NAME }} 319 | cache-to: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true 320 | build-args: | 321 | ${{ env.DOCKER_IMAGE_ARGUMENTS }} 322 | tags: | 323 | ${{ env.DOCKER_CACHE_GRYPE }} 324 | 325 | # SCAN LOCAL CONTAINER IMAGE WITH GRYPE 326 | - name: container image / scan with grype 327 | if: env.WORKFLOW_BUILD == 'true' 328 | id: grype 329 | uses: anchore/scan-action@1638637db639e0ade3258b51db49a9a137574c3e # v6.5.1 330 | with: 331 | image: ${{ env.DOCKER_CACHE_GRYPE }} 332 | fail-build: ${{ env.WORKFLOW_GRYPE_FAIL_ON_SEVERITY }} 333 | severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} 334 | output-format: 'sarif' 335 | by-cve: true 336 | cache-db: true 337 | 338 | # OUTPUT CVE REPORT IF SCAN FAILS 339 | - name: container image / scan with grype FAILED 340 | if: env.WORKFLOW_BUILD == 'true' && (failure() || steps.grype.outcome == 'failure') && steps.image-build.outcome == 'success' 341 | uses: anchore/scan-action@1638637db639e0ade3258b51db49a9a137574c3e # v6.5.1 342 | with: 343 | image: ${{ env.DOCKER_CACHE_GRYPE }} 344 | fail-build: false 345 | severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} 346 | output-format: 'table' 347 | by-cve: true 348 | cache-db: true 349 | 350 | # PUSH IMAGE TO ALL REGISTRIES IF CLEAN 351 | - name: container image / push to registries 352 | id: image-push 353 | if: env.WORKFLOW_BUILD == 'true' 354 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 355 | with: 356 | context: . 357 | file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} 358 | push: true 359 | sbom: true 360 | provenance: mode=max 361 | platforms: linux/${{ matrix.platform }} 362 | cache-from: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }} 363 | cache-to: type=registry,ref=${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true 364 | build-args: | 365 | ${{ env.DOCKER_IMAGE_ARGUMENTS }} 366 | tags: | 367 | ${{ env.DOCKER_IMAGE_TAGS }} 368 | 369 | # CREATE ATTESTATION ARTIFACTS 370 | - name: container image / create attestation artifacts 371 | if: env.WORKFLOW_BUILD == 'true' && steps.image-push.outcome == 'success' 372 | uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 373 | with: 374 | subject-name: docker.io/${{ env.DOCKER_IMAGE_NAME }} 375 | subject-digest: ${{ steps.image-push.outputs.digest }} 376 | push-to-registry: false 377 | 378 | # EXPORT DIGEST 379 | - name: container image / export digest 380 | if: env.WORKFLOW_BUILD == 'true' && steps.image-push.outcome == 'success' 381 | run: | 382 | mkdir -p ${{ runner.temp }}/digests 383 | digest="${{ steps.image-push.outputs.digest }}" 384 | touch "${{ runner.temp }}/digests/${digest#sha256:}" 385 | 386 | # UPLOAD DIGEST 387 | - name: container image / upload 388 | if: env.WORKFLOW_BUILD == 'true' && steps.image-push.outcome == 'success' 389 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 390 | with: 391 | name: digests-linux-${{ env.DOCKER_IMAGE_PLATFORM_SANITIZED }} 392 | path: ${{ runner.temp }}/digests/* 393 | if-no-files-found: error 394 | 395 | 396 | # ╔═════════════════════════════════════════════════════╗ 397 | # ║ CREATE GITHUB RELEASE ║ 398 | # ╚═════════════════════════════════════════════════════╝ 399 | # CREATE RELEASE MARKUP 400 | - name: github release / prepare markdown 401 | if: env.WORKFLOW_CREATE_RELEASE == 'true' && matrix.platform == 'amd64' 402 | id: git-release 403 | uses: 11notes/action-docker-release@v1 404 | 405 | # CREATE GITHUB RELEASE 406 | - name: github release / create 407 | if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-release.outcome == 'success' 408 | uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 409 | env: 410 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 411 | with: 412 | tag_name: ${{ github.ref }} 413 | release_name: ${{ github.ref }} 414 | body: ${{ steps.git-release.outputs.release }} 415 | draft: false 416 | prerelease: false 417 | 418 | 419 | # ╔═════════════════════════════════════════════════════╗ 420 | # ║ CREATE README.md ║ 421 | # ╚═════════════════════════════════════════════════════╝ 422 | # CHECKOUT HEAD TO BE UP TO DATE WITH EVERYTHING 423 | - name: README.md / checkout 424 | if: env.WORKFLOW_CREATE_README == 'true' && matrix.platform == 'amd64' 425 | continue-on-error: true 426 | run: | 427 | git checkout HEAD 428 | 429 | # CREATE MAKRDOWN OF README.md 430 | - name: README.md / create 431 | id: github-readme 432 | continue-on-error: true 433 | if: env.WORKFLOW_CREATE_README == 'true' && matrix.platform == 'amd64' 434 | uses: 11notes/action-docker-readme@v1 435 | with: 436 | sarif_file: ${{ steps.grype.outputs.sarif }} 437 | build_output_metadata: ${{ steps.image-build.outputs.metadata }} 438 | 439 | # UPLOAD README.md to DOCKER HUB 440 | - name: README.md / push to Docker Hub 441 | continue-on-error: true 442 | if: env.WORKFLOW_CREATE_README == 'true' && matrix.platform == 'amd64' && steps.github-readme.outcome == 'success' && hashFiles('README_NONGITHUB.md') != '' 443 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 444 | env: 445 | DOCKER_USER: 11notes 446 | DOCKER_PASS: ${{ secrets.DOCKER_TOKEN }} 447 | with: 448 | destination_container_repo: ${{ env.DOCKER_IMAGE_NAME }} 449 | provider: dockerhub 450 | short_description: ${{ env.DOCKER_IMAGE_DESCRIPTION }} 451 | readme_file: 'README_NONGITHUB.md' 452 | 453 | # COMMIT NEW README.md, LICENSE and compose 454 | - name: README.md / github commit & push 455 | continue-on-error: true 456 | if: env.WORKFLOW_CREATE_README == 'true' && matrix.platform == 'amd64' && steps.github-readme.outcome == 'success' && hashFiles('README.md') != '' 457 | run: | 458 | git config user.name "github-actions[bot]" 459 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 460 | git add README.md 461 | if [ -f compose.yaml ]; then 462 | git add compose.yaml 463 | fi 464 | if [ -f compose.yml ]; then 465 | git add compose.yml 466 | fi 467 | if [ -f LICENSE ]; then 468 | git add LICENSE 469 | fi 470 | git commit -m "update README.md" 471 | git push origin HEAD:master 472 | 473 | 474 | # ╔═════════════════════════════════════════════════════╗ 475 | # ║ GITHUB REPOSITORY DEFAULT SETTINGS ║ 476 | # ╚═════════════════════════════════════════════════════╝ 477 | # UPDATE REPO WITH DEFAULT SETTINGS FOR CONTAINER IMAGE 478 | - name: github / update description and set repo defaults 479 | if: matrix.platform == 'amd64' 480 | run: | 481 | curl --request PATCH \ 482 | --url https://api.github.com/repos/${{ github.repository }} \ 483 | --header 'authorization: Bearer ${{ secrets.REPOSITORY_TOKEN }}' \ 484 | --header 'content-type: application/json' \ 485 | --data '{ 486 | "description":"${{ env.DOCKER_IMAGE_DESCRIPTION }}", 487 | "homepage":"", 488 | "has_issues":true, 489 | "has_discussions":true, 490 | "has_projects":false, 491 | "has_wiki":false 492 | }' \ 493 | --fail 494 | 495 | 496 | # ╔═════════════════════════════════════════════════════╗ 497 | # ║ ║ 498 | # ║ ║ 499 | # ║ MERGE IMAGES INTO SINGLE MANIFEST ║ 500 | # ║ ║ 501 | # ║ ║ 502 | # ╚═════════════════════════════════════════════════════╝ 503 | merge_platform_images: 504 | needs: docker 505 | if: needs.docker.outputs.WORKFLOW_BUILD == 'true' 506 | name: merge platform images to a single manifest 507 | runs-on: ubuntu-latest 508 | strategy: 509 | fail-fast: false 510 | matrix: 511 | registry: [docker.io, ghcr.io, quay.io] 512 | 513 | env: 514 | DOCKER_IMAGE_NAME: ${{ needs.docker.outputs.DOCKER_IMAGE_NAME }} 515 | DOCKER_IMAGE_MERGE_TAGS: ${{ needs.docker.outputs.DOCKER_IMAGE_MERGE_TAGS }} 516 | 517 | permissions: 518 | contents: read 519 | packages: write 520 | attestations: write 521 | id-token: write 522 | 523 | steps: 524 | # ╔═════════════════════════════════════════════════════╗ 525 | # ║ CONTAINER REGISTRY LOGIN ║ 526 | # ╚═════════════════════════════════════════════════════╝ 527 | # DOCKER HUB 528 | - name: docker / login to hub 529 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 530 | with: 531 | username: 11notes 532 | password: ${{ secrets.DOCKER_TOKEN }} 533 | 534 | # GITHUB CONTAINER REGISTRY 535 | - name: github / login to ghcr 536 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 537 | with: 538 | registry: ghcr.io 539 | username: 11notes 540 | password: ${{ secrets.GITHUB_TOKEN }} 541 | 542 | # REDHAT QUAY 543 | - name: quay / login to quay 544 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 545 | with: 546 | registry: quay.io 547 | username: 11notes+github 548 | password: ${{ secrets.QUAY_TOKEN }} 549 | 550 | 551 | # ╔═════════════════════════════════════════════════════╗ 552 | # ║ MERGE PLATFORM IMAGES MANIFEST ║ 553 | # ╚═════════════════════════════════════════════════════╝ 554 | # DOWNLOAD DIGESTS 555 | - name: platform merge / digest 556 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 557 | with: 558 | path: ${{ runner.temp }}/digests 559 | pattern: digests-* 560 | merge-multiple: true 561 | 562 | # SETUP BUILDX BUILDER 563 | - name: platform merge / buildx 564 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 565 | 566 | # GET META DATA 567 | - name: platform merge / meta 568 | id: meta 569 | uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 570 | with: 571 | images: ${{ matrix.registry }}/${{ env.DOCKER_IMAGE_NAME }} 572 | tags: | 573 | ${{ env.DOCKER_IMAGE_MERGE_TAGS }} 574 | 575 | # CREATE MANIFEST 576 | - name: platform merge / create manifest and push 577 | working-directory: ${{ runner.temp }}/digests 578 | run: | 579 | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 580 | $(printf 'docker.io/${{ env.DOCKER_IMAGE_NAME }}@sha256:%s ' *) 581 | 582 | # INSPECT MANIFEST 583 | - name: platform merge / inspect 584 | run: | 585 | docker buildx imagetools inspect ${{ matrix.registry }}/${{ env.DOCKER_IMAGE_NAME }}:${{ steps.meta.outputs.version }} --------------------------------------------------------------------------------