├── .gitignore ├── .gitattributes ├── .dockerignore ├── .github └── workflows │ ├── tags.yml │ ├── readme.yml │ ├── version.yml │ ├── cve.yml │ ├── cron.update.yml │ └── docker.yml ├── .json ├── LICENSE ├── project.md ├── compose.yml ├── arch.dockerfile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # default 2 | maintain/ 3 | node_modules/ 4 | 5 | # custom 6 | .env -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # default 2 | * text=auto 3 | *.sh eol=lf -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # default 2 | .git* 3 | maintain/ 4 | LICENSE 5 | *.md 6 | img/ 7 | node_modules/ -------------------------------------------------------------------------------- /.github/workflows/tags.yml: -------------------------------------------------------------------------------- 1 | name: tags 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | tags: 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", "platform":"amd64,arm64" }' -------------------------------------------------------------------------------- /.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/pocket-id", 3 | "name": "pocket-id", 4 | "root": "/pocket-id", 5 | "semver": { 6 | "version": "1.16.0" 7 | }, 8 | "readme": { 9 | "description": "Run pocket-id rootless and distroless.", 10 | "introduction": "Pocket ID is a simple OIDC provider that allows users to authenticate with their passkeys to your services.", 11 | "built": { 12 | "pocket-id/pocket-id": "https://github.com/pocket-id/pocket-id" 13 | }, 14 | "distroless": { 15 | "layers": [ 16 | "11notes/distroless" 17 | ] 18 | }, 19 | "comparison": { 20 | "image": "ghcr.io/pocket-id/pocket-id" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /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 }}" }' -------------------------------------------------------------------------------- /project.md: -------------------------------------------------------------------------------- 1 | ${{ content_synopsis }} This image will run pocket-id [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. 2 | 3 | ${{ content_uvp }} Good question! Because ... 4 | 5 | ${{ github:> [!IMPORTANT] }} 6 | ${{ github:> }}* ... this image runs [rootless](https://github.com/11notes/RTFM/blob/main/linux/container/image/rootless.md) as 1000:1000 7 | ${{ github:> }}* ... this image has no shell since it is [distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md) 8 | ${{ github:> }}* ... this image has a health check 9 | ${{ github:> }}* ... this image runs read-only 10 | ${{ github:> }}* ... this image is automatically scanned for CVEs before and after publishing 11 | ${{ github:> }}* ... this image is created via a secure and pinned CI/CD process 12 | ${{ github:> }}* ... this image is very small 13 | 14 | If you value security, simplicity and optimizations to the extreme, then this image might be for you. 15 | 16 | ${{ content_comparison }} 17 | 18 | ${{ title_volumes }} 19 | * **${{ json_root }}/var** - Directory of your keys, uploads and geolite database (if license is set) 20 | 21 | ${{ content_compose }} 22 | 23 | ${{ content_defaults }} 24 | 25 | ${{ content_environment }} 26 | 27 | ${{ content_source }} 28 | 29 | ${{ content_parent }} 30 | 31 | ${{ content_built }} 32 | 33 | ${{ content_tips }} -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | name: "idp" 2 | 3 | x-lockdown: &lockdown 4 | # prevents write access to the image itself 5 | read_only: true 6 | # prevents any process within the container to gain more privileges 7 | security_opt: 8 | - "no-new-privileges=true" 9 | 10 | services: 11 | postgres: 12 | # detailed info about this image: https://github.com/11notes/docker-postgres 13 | image: "11notes/postgres:16" 14 | <<: *lockdown 15 | environment: 16 | TZ: "Europe/Zurich" 17 | POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" 18 | POSTGRES_BACKUP_SCHEDULE: "0 3 * * *" 19 | volumes: 20 | - "postgres.etc:/postgres/etc" 21 | - "postgres.var:/postgres/var" 22 | - "postgres.backup:/postgres/backup" 23 | tmpfs: 24 | - "/postgres/run:uid=1000,gid=1000" 25 | - "/postgres/log:uid=1000,gid=1000" 26 | networks: 27 | backend: 28 | restart: "always" 29 | 30 | pocket-id: 31 | depends_on: 32 | postgres: 33 | condition: "service_healthy" 34 | restart: true 35 | image: "11notes/pocket-id:1.16.0" 36 | <<: *lockdown 37 | environment: 38 | TZ: "Europe/Zurich" 39 | APP_URL: "${FQDN}" 40 | TRUST_PROXY: true 41 | DB_PROVIDER: "postgres" 42 | DB_CONNECTION_STRING: "postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres" 43 | # DB_CONNECTION_STRING: "file:/pocket-id/var/pocket-id.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate" 44 | volumes: 45 | - "pocket-id.var:/pocket-id/var" 46 | ports: 47 | - "3000:1411/tcp" 48 | networks: 49 | frontend: 50 | backend: 51 | restart: "always" 52 | 53 | volumes: 54 | postgres.etc: 55 | postgres.var: 56 | postgres.backup: 57 | pocket-id.var: 58 | 59 | networks: 60 | frontend: 61 | backend: 62 | internal: true -------------------------------------------------------------------------------- /.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_SRC=pocket-id/pocket-id.git \ 8 | BUILD_ROOT=/go/pocket-id 9 | ARG BUILD_BIN=${BUILD_ROOT}/backend/pocket-id 10 | 11 | # :: FOREIGN IMAGES 12 | FROM 11notes/distroless AS distroless 13 | FROM 11notes/util AS util 14 | 15 | # ╔═════════════════════════════════════════════════════╗ 16 | # ║ BUILD ║ 17 | # ╚═════════════════════════════════════════════════════╝ 18 | # :: POCKET-ID 19 | FROM 11notes/go:1.25 AS build 20 | ARG APP_VERSION \ 21 | BUILD_SRC \ 22 | BUILD_ROOT \ 23 | BUILD_BIN 24 | 25 | RUN set -ex; \ 26 | apk --update --no-cache add \ 27 | nodejs \ 28 | npm \ 29 | pnpm \ 30 | yarn; 31 | 32 | RUN set -ex; \ 33 | eleven git clone ${BUILD_SRC} v${APP_VERSION}; 34 | 35 | RUN set -ex; \ 36 | cd ${BUILD_ROOT}/frontend; \ 37 | pnpm install; \ 38 | BUILD_OUTPUT_PATH=dist npm run build; 39 | 40 | RUN set -ex; \ 41 | cd ${BUILD_ROOT}/backend/cmd; \ 42 | cp -R ${BUILD_ROOT}/frontend/dist ${BUILD_ROOT}/backend/frontend/dist; \ 43 | go build -trimpath -ldflags="-X github.com/pocket-id/pocket-id/backend/internal/common.Version=${APP_VERSION} -buildid=${APP_VERSION} -extldflags=-static" -o ${BUILD_BIN} main.go; 44 | 45 | RUN set -ex; \ 46 | eleven distroless ${BUILD_BIN}; 47 | 48 | # :: FILE SYSTEM 49 | FROM alpine AS file-system 50 | COPY --from=util / / 51 | ARG APP_ROOT 52 | RUN set -ex; \ 53 | eleven mkdir /distroless${APP_ROOT}/var/{uploads,keys,geolite}; 54 | 55 | # ╔═════════════════════════════════════════════════════╗ 56 | # ║ IMAGE ║ 57 | # ╚═════════════════════════════════════════════════════╝ 58 | # :: HEADER 59 | FROM scratch 60 | 61 | # :: default arguments 62 | ARG TARGETPLATFORM \ 63 | TARGETOS \ 64 | TARGETARCH \ 65 | TARGETVARIANT \ 66 | APP_IMAGE \ 67 | APP_NAME \ 68 | APP_VERSION \ 69 | APP_ROOT \ 70 | APP_UID \ 71 | APP_GID \ 72 | APP_NO_CACHE 73 | 74 | # :: default environment 75 | ENV APP_IMAGE=${APP_IMAGE} \ 76 | APP_NAME=${APP_NAME} \ 77 | APP_VERSION=${APP_VERSION} \ 78 | APP_ROOT=${APP_ROOT} 79 | 80 | # :: app specific environment 81 | ENV APP_ENV=production \ 82 | ANALYTICS_DISABLED=true \ 83 | UPLOAD_PATH=${APP_ROOT}/var/uploads \ 84 | KEYS_PATH=${APP_ROOT}/var/keys \ 85 | GEOLITE_DB_PATH=${APP_ROOT}/var/geolite; 86 | 87 | # :: multi-stage 88 | COPY --from=distroless / / 89 | COPY --from=build /distroless/ / 90 | COPY --from=file-system --chown=${APP_UID}:${APP_GID} /distroless/ / 91 | 92 | # :: PERSISTENT DATA 93 | VOLUME ["${APP_ROOT}/var"] 94 | 95 | # :: HEALTH 96 | HEALTHCHECK --interval=5s --timeout=2s --start-interval=5s \ 97 | CMD ["/usr/local/bin/pocket-id", "healthcheck"] 98 | 99 | # :: EXECUTE 100 | USER ${APP_UID}:${APP_GID} 101 | ENTRYPOINT ["/usr/local/bin/pocket-id"] -------------------------------------------------------------------------------- /.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/pocket-id/pocket-id/releases/latest | jq -r '.tag_name' | 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 | try{ 75 | writeFileSync(resolve('.json'), JSON.stringify(repository.dot, null, 2)); 76 | core.exportVariable('WORKFLOW_AUTO_UPDATE', true); 77 | }catch(e){ 78 | core.setFailed(e); 79 | } 80 | }else{ 81 | core.info('no new release found'); 82 | } 83 | 84 | core.info(inspect(repository.dot, {showHidden:false, depth:null, colors:true})); 85 | 86 | - name: cron-update / checkout 87 | id: checkout 88 | if: env.WORKFLOW_AUTO_UPDATE == 'true' 89 | run: | 90 | git config user.name "github-actions[bot]" 91 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 92 | git add .json 93 | git commit -m "chore: auto upgrade to ${{ env.LATEST_VERSION }}" 94 | git push origin HEAD:master 95 | 96 | - name: cron-update / tag 97 | if: env.WORKFLOW_AUTO_UPDATE == 'true' && steps.checkout.outcome == 'success' 98 | run: | 99 | SHA256=$(git rev-list --branches --max-count=1) 100 | git tag -a v${{ env.WORKFLOW_NEW_TAG }} -m "v${{ env.WORKFLOW_NEW_TAG }}" ${SHA256} 101 | git push --follow-tags 102 | 103 | - name: cron-update / build docker image 104 | if: env.WORKFLOW_AUTO_UPDATE == 'true' && steps.checkout.outcome == 'success' 105 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 106 | with: 107 | workflow: docker.yml 108 | wait-for-completion: false 109 | token: "${{ secrets.REPOSITORY_TOKEN }}" 110 | inputs: '{ "release":"true", "readme":"true", "platform":"amd64,arm64" }' 111 | 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 | # POCKET-ID 4 | ![size](https://img.shields.io/badge/image_size-31MB-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/pocket-id?color=2b75d6)![5px](https://raw.githubusercontent.com/11notes/static/refs/heads/main/img/markdown/transparent5x2px.png)[](https://github.com/11notes/docker-pocket-id/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 pocket-id rootless and distroless. 7 | 8 | # INTRODUCTION 📢 9 | 10 | Pocket ID is a simple OIDC provider that allows users to authenticate with their passkeys to your services. 11 | 12 | # SYNOPSIS 📖 13 | **What can I do with this?** This image will run pocket-id [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. 14 | 15 | # UNIQUE VALUE PROPOSITION 💶 16 | **Why should I run this image and not the other image(s) that already exist?** Good question! Because ... 17 | 18 | > [!IMPORTANT] 19 | >* ... this image runs [rootless](https://github.com/11notes/RTFM/blob/main/linux/container/image/rootless.md) as 1000:1000 20 | >* ... this image has no shell since it is [distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md) 21 | >* ... this image has a health check 22 | >* ... this image runs read-only 23 | >* ... this image is automatically scanned for CVEs before and after publishing 24 | >* ... this image is created via a secure and pinned CI/CD process 25 | >* ... this image is very small 26 | 27 | If you value security, simplicity and optimizations to the extreme, then this image might be for you. 28 | 29 | # COMPARISON 🏁 30 | Below you find a comparison between this image and the most used or original one. 31 | 32 | | **image** | **size on disk** | **init default as** | **[distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md)** | supported architectures 33 | | ---: | ---: | :---: | :---: | :---: | 34 | | 11notes/pocket-id | 31MB | 1000:1000 | ✅ | amd64, arm64 | 35 | | pocket-id/pocket-id | 72MB | 0:0 | ❌ | amd64, arm64 | 36 | 37 | # VOLUMES 📁 38 | * **/pocket-id/var** - Directory of your keys, uploads and geolite database (if license is set) 39 | 40 | # COMPOSE ✂️ 41 | ```yaml 42 | name: "idp" 43 | 44 | x-lockdown: &lockdown 45 | # prevents write access to the image itself 46 | read_only: true 47 | # prevents any process within the container to gain more privileges 48 | security_opt: 49 | - "no-new-privileges=true" 50 | 51 | services: 52 | postgres: 53 | # detailed info about this image: https://github.com/11notes/docker-postgres 54 | image: "11notes/postgres:16" 55 | <<: *lockdown 56 | environment: 57 | TZ: "Europe/Zurich" 58 | POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" 59 | POSTGRES_BACKUP_SCHEDULE: "0 3 * * *" 60 | volumes: 61 | - "postgres.etc:/postgres/etc" 62 | - "postgres.var:/postgres/var" 63 | - "postgres.backup:/postgres/backup" 64 | tmpfs: 65 | - "/postgres/run:uid=1000,gid=1000" 66 | - "/postgres/log:uid=1000,gid=1000" 67 | networks: 68 | backend: 69 | restart: "always" 70 | 71 | pocket-id: 72 | depends_on: 73 | postgres: 74 | condition: "service_healthy" 75 | restart: true 76 | image: "11notes/pocket-id:1.16.0" 77 | <<: *lockdown 78 | environment: 79 | TZ: "Europe/Zurich" 80 | APP_URL: "${FQDN}" 81 | TRUST_PROXY: true 82 | DB_PROVIDER: "postgres" 83 | DB_CONNECTION_STRING: "postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres" 84 | # DB_CONNECTION_STRING: "file:/pocket-id/var/pocket-id.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate" 85 | volumes: 86 | - "pocket-id.var:/pocket-id/var" 87 | ports: 88 | - "3000:1411/tcp" 89 | networks: 90 | frontend: 91 | backend: 92 | restart: "always" 93 | 94 | volumes: 95 | postgres.etc: 96 | postgres.var: 97 | postgres.backup: 98 | pocket-id.var: 99 | 100 | networks: 101 | frontend: 102 | backend: 103 | internal: true 104 | ``` 105 | 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). 106 | 107 | # DEFAULT SETTINGS 🗃️ 108 | | Parameter | Value | Description | 109 | | --- | --- | --- | 110 | | `user` | docker | user name | 111 | | `uid` | 1000 | [user identifier](https://en.wikipedia.org/wiki/User_identifier) | 112 | | `gid` | 1000 | [group identifier](https://en.wikipedia.org/wiki/Group_identifier) | 113 | | `home` | /pocket-id | home directory of user docker | 114 | 115 | # ENVIRONMENT 📝 116 | | Parameter | Value | Default | 117 | | --- | --- | --- | 118 | | `TZ` | [Time Zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | | 119 | | `DEBUG` | Will activate debug option for container image and app (if available) | | 120 | 121 | # MAIN TAGS 🏷️ 122 | These are the main tags for the image. There is also a tag for each commit and its shorthand sha256 value. 123 | 124 | * [1.16.0](https://hub.docker.com/r/11notes/pocket-id/tags?name=1.16.0) 125 | 126 | ### There is no latest tag, what am I supposed to do about updates? 127 | 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 ```:1.16.0``` you can use ```:1``` or ```:1.16```. 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. 128 | 129 | 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! 130 | 131 | # REGISTRIES ☁️ 132 | ``` 133 | docker pull 11notes/pocket-id:1.16.0 134 | docker pull ghcr.io/11notes/pocket-id:1.16.0 135 | docker pull quay.io/11notes/pocket-id:1.16.0 136 | ``` 137 | 138 | # SOURCE 💾 139 | * [11notes/pocket-id](https://github.com/11notes/docker-pocket-id) 140 | 141 | # PARENT IMAGE 🏛️ 142 | > [!IMPORTANT] 143 | >This image is not based on another image but uses [scratch](https://hub.docker.com/_/scratch) as the starting layer. 144 | >The image consists of the following distroless layers that were added: 145 | >* [11notes/distroless](https://github.com/11notes/docker-distroless/blob/master/arch.dockerfile) - contains users, timezones and Root CA certificates, nothing else 146 | 147 | # BUILT WITH 🧰 148 | * [pocket-id/pocket-id](https://github.com/pocket-id/pocket-id) 149 | 150 | # GENERAL TIPS 📌 151 | > [!TIP] 152 | >* Use a reverse proxy like Traefik, Nginx, HAproxy to terminate TLS and to protect your endpoints 153 | >* Use Let’s Encrypt DNS-01 challenge to obtain valid SSL certificates for your services 154 | 155 | # ElevenNotes™️ 156 | 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-pocket-id/releases) for breaking changes. If you have any problems with using this image simply raise an [issue](https://github.com/11notes/docker-pocket-id/issues), thanks. If you have a question or inputs please create a new [discussion](https://github.com/11notes/docker-pocket-id/discussions) instead of an issue. You can find all my other repositories on [github](https://github.com/11notes?tab=repositories). 157 | 158 | *created 01.12.2025, 09:58:44 (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 | const docker = { 161 | image:{ 162 | name:opt.dot.image, 163 | arch:(opt.input?.etc?.arch || opt.dot?.arch || 'linux/amd64,linux/arm64'), 164 | prefix:((opt.input?.etc?.semverprefix) ? `${opt.input?.etc?.semverprefix}-` : ''), 165 | suffix:((opt.input?.etc?.semversuffix) ? `-${opt.input?.etc?.semversuffix}` : ''), 166 | description:(opt.dot?.readme?.description || ''), 167 | platform:{ 168 | sanitized:"${{ matrix.platform }}".replace(/[^A-Z-a-z0-9]+/i, ""), 169 | }, 170 | tags:[], 171 | build:(opt.input?.build === undefined) ? false : opt.input.build, 172 | }, 173 | app:{ 174 | image:opt.dot.image, 175 | name:opt.dot.name, 176 | version:(opt.input?.etc?.version || opt.dot?.semver?.version), 177 | root:opt.dot.root, 178 | UID:(opt.input?.etc?.uid || 1000), 179 | GID:(opt.input?.etc?.gid || 1000), 180 | no_cache:new Date().getTime(), 181 | }, 182 | cache:{ 183 | registry:'localhost:5000/', 184 | enable:(opt.input?.etc?.cache === undefined) ? true : opt.input.etc.cache, 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', docker.image.build); 256 | core.setOutput('WORKFLOW_BUILD', docker.image.build); 257 | core.exportVariable('WORKFLOW_BUILD_NO_CACHE', !docker.cache.enable); 258 | 259 | core.exportVariable('WORKFLOW_CREATE_RELEASE', (opt.input?.release === undefined) ? false : opt.input.release); 260 | core.exportVariable('WORKFLOW_CREATE_README', (opt.input?.readme === undefined) ? false : opt.input.readme); 261 | core.exportVariable('WORKFLOW_GRYPE_FAIL_ON_SEVERITY', (opt.dot?.grype?.fail === undefined) ? true : opt.dot.grype.fail); 262 | core.exportVariable('WORKFLOW_GRYPE_SEVERITY_CUTOFF', (opt.dot?.grype?.severity || 'high')); 263 | 264 | // print 265 | core.info(inspect({opt:opt, docker:docker}, {showHidden:false, depth:null, colors:true})); 266 | 267 | 268 | # ╔═════════════════════════════════════════════════════╗ 269 | # ║ CONTAINER REGISTRY LOGIN ║ 270 | # ╚═════════════════════════════════════════════════════╝ 271 | # DOCKER HUB 272 | - name: docker / login to hub 273 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 274 | with: 275 | username: 11notes 276 | password: ${{ secrets.DOCKER_TOKEN }} 277 | 278 | # GITHUB CONTAINER REGISTRY 279 | - name: github / login to ghcr 280 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 281 | with: 282 | registry: ghcr.io 283 | username: 11notes 284 | password: ${{ secrets.GITHUB_TOKEN }} 285 | 286 | # REDHAT QUAY 287 | - name: quay / login to quay 288 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 289 | with: 290 | registry: quay.io 291 | username: 11notes+github 292 | password: ${{ secrets.QUAY_TOKEN }} 293 | 294 | 295 | # ╔═════════════════════════════════════════════════════╗ 296 | # ║ BUILD CONTAINER IMAGE ║ 297 | # ╚═════════════════════════════════════════════════════╝ 298 | # SETUP QEMU 299 | - name: container image / setup qemu 300 | if: env.WORKFLOW_BUILD == 'true' 301 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 302 | with: 303 | image: tonistiigi/binfmt:qemu-v8.1.5 304 | cache-image: false 305 | 306 | # SETUP BUILDX BUILDER WITH USING LOCAL REGISTRY 307 | - name: container image / setup buildx 308 | if: env.WORKFLOW_BUILD == 'true' 309 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 310 | with: 311 | driver-opts: network=host 312 | 313 | # BUILD CONTAINER IMAGE FROM GLOBAL CACHE (DOCKER HUB) AND PUSH TO LOCAL CACHE 314 | - name: container image / build 315 | if: env.WORKFLOW_BUILD == 'true' 316 | id: image-build 317 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 318 | with: 319 | context: . 320 | no-cache: ${{ env.WORKFLOW_BUILD_NO_CACHE }} 321 | file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} 322 | push: true 323 | platforms: linux/${{ matrix.platform }} 324 | cache-from: type=registry,ref=${{ env.DOCKER_CACHE_NAME }} 325 | cache-to: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true 326 | build-args: | 327 | ${{ env.DOCKER_IMAGE_ARGUMENTS }} 328 | tags: | 329 | ${{ env.DOCKER_CACHE_GRYPE }} 330 | 331 | # SCAN LOCAL CONTAINER IMAGE WITH GRYPE 332 | - name: container image / scan with grype 333 | if: env.WORKFLOW_BUILD == 'true' 334 | id: grype 335 | uses: anchore/scan-action@1638637db639e0ade3258b51db49a9a137574c3e # v6.5.1 336 | with: 337 | image: ${{ env.DOCKER_CACHE_GRYPE }} 338 | fail-build: ${{ env.WORKFLOW_GRYPE_FAIL_ON_SEVERITY }} 339 | severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} 340 | output-format: 'sarif' 341 | by-cve: true 342 | cache-db: true 343 | 344 | # OUTPUT CVE REPORT IF SCAN FAILS 345 | - name: container image / scan with grype FAILED 346 | if: env.WORKFLOW_BUILD == 'true' && (failure() || steps.grype.outcome == 'failure') && steps.image-build.outcome == 'success' 347 | uses: anchore/scan-action@1638637db639e0ade3258b51db49a9a137574c3e # v6.5.1 348 | with: 349 | image: ${{ env.DOCKER_CACHE_GRYPE }} 350 | fail-build: false 351 | severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} 352 | output-format: 'table' 353 | by-cve: true 354 | cache-db: true 355 | 356 | # PUSH IMAGE TO ALL REGISTRIES IF CLEAN 357 | - name: container image / push to registries 358 | id: image-push 359 | if: env.WORKFLOW_BUILD == 'true' 360 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 361 | with: 362 | context: . 363 | no-cache: ${{ env.WORKFLOW_BUILD_NO_CACHE }} 364 | file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} 365 | push: true 366 | sbom: true 367 | provenance: mode=max 368 | platforms: linux/${{ matrix.platform }} 369 | cache-from: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }} 370 | cache-to: type=registry,ref=${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true 371 | build-args: | 372 | ${{ env.DOCKER_IMAGE_ARGUMENTS }} 373 | tags: | 374 | ${{ env.DOCKER_IMAGE_TAGS }} 375 | 376 | # CREATE ATTESTATION ARTIFACTS 377 | - name: container image / create attestation artifacts 378 | if: env.WORKFLOW_BUILD == 'true' && steps.image-push.outcome == 'success' 379 | uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 380 | with: 381 | subject-name: docker.io/${{ env.DOCKER_IMAGE_NAME }} 382 | subject-digest: ${{ steps.image-push.outputs.digest }} 383 | push-to-registry: false 384 | 385 | # EXPORT DIGEST 386 | - name: container image / export digest 387 | if: env.WORKFLOW_BUILD == 'true' && steps.image-push.outcome == 'success' 388 | run: | 389 | mkdir -p ${{ runner.temp }}/digests 390 | digest="${{ steps.image-push.outputs.digest }}" 391 | touch "${{ runner.temp }}/digests/${digest#sha256:}" 392 | 393 | # UPLOAD DIGEST 394 | - name: container image / upload 395 | if: env.WORKFLOW_BUILD == 'true' && steps.image-push.outcome == 'success' 396 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 397 | with: 398 | name: digests-linux-${{ env.DOCKER_IMAGE_PLATFORM_SANITIZED }} 399 | path: ${{ runner.temp }}/digests/* 400 | if-no-files-found: error 401 | 402 | 403 | # ╔═════════════════════════════════════════════════════╗ 404 | # ║ CREATE GITHUB RELEASE ║ 405 | # ╚═════════════════════════════════════════════════════╝ 406 | # CREATE RELEASE MARKUP 407 | - name: github release / prepare markdown 408 | if: env.WORKFLOW_CREATE_RELEASE == 'true' && matrix.platform == 'amd64' 409 | id: git-release 410 | uses: 11notes/action-docker-release@v1 411 | 412 | # CREATE GITHUB RELEASE 413 | - name: github release / create 414 | if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-release.outcome == 'success' 415 | uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 416 | env: 417 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 418 | with: 419 | tag_name: ${{ github.ref }} 420 | release_name: ${{ github.ref }} 421 | body: ${{ steps.git-release.outputs.release }} 422 | draft: false 423 | prerelease: false 424 | 425 | 426 | # ╔═════════════════════════════════════════════════════╗ 427 | # ║ CREATE README.md ║ 428 | # ╚═════════════════════════════════════════════════════╝ 429 | # CHECKOUT HEAD TO BE UP TO DATE WITH EVERYTHING 430 | - name: README.md / checkout 431 | if: env.WORKFLOW_CREATE_README == 'true' && matrix.platform == 'amd64' 432 | continue-on-error: true 433 | run: | 434 | git checkout HEAD 435 | 436 | # CREATE MAKRDOWN OF README.md 437 | - name: README.md / create 438 | id: github-readme 439 | continue-on-error: true 440 | if: env.WORKFLOW_CREATE_README == 'true' && matrix.platform == 'amd64' 441 | uses: 11notes/action-docker-readme@v1 442 | with: 443 | sarif_file: ${{ steps.grype.outputs.sarif }} 444 | build_output_metadata: ${{ steps.image-build.outputs.metadata }} 445 | 446 | # UPLOAD README.md to DOCKER HUB 447 | - name: README.md / push to Docker Hub 448 | continue-on-error: true 449 | if: env.WORKFLOW_CREATE_README == 'true' && matrix.platform == 'amd64' && steps.github-readme.outcome == 'success' && hashFiles('README_NONGITHUB.md') != '' 450 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 451 | env: 452 | DOCKER_USER: 11notes 453 | DOCKER_PASS: ${{ secrets.DOCKER_TOKEN }} 454 | with: 455 | destination_container_repo: ${{ env.DOCKER_IMAGE_NAME }} 456 | provider: dockerhub 457 | short_description: ${{ env.DOCKER_IMAGE_DESCRIPTION }} 458 | readme_file: 'README_NONGITHUB.md' 459 | 460 | # COMMIT NEW README.md, LICENSE and compose 461 | - name: README.md / github commit & push 462 | continue-on-error: true 463 | if: env.WORKFLOW_CREATE_README == 'true' && matrix.platform == 'amd64' && steps.github-readme.outcome == 'success' && hashFiles('README.md') != '' 464 | run: | 465 | git config user.name "github-actions[bot]" 466 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 467 | git add README.md 468 | if [ -f compose.yaml ]; then 469 | git add compose.yaml 470 | fi 471 | if [ -f compose.yml ]; then 472 | git add compose.yml 473 | fi 474 | if [ -f LICENSE ]; then 475 | git add LICENSE 476 | fi 477 | git commit -m "update README.md" 478 | git push origin HEAD:master 479 | 480 | 481 | # ╔═════════════════════════════════════════════════════╗ 482 | # ║ GITHUB REPOSITORY DEFAULT SETTINGS ║ 483 | # ╚═════════════════════════════════════════════════════╝ 484 | # UPDATE REPO WITH DEFAULT SETTINGS FOR CONTAINER IMAGE 485 | - name: github / update description and set repo defaults 486 | if: matrix.platform == 'amd64' 487 | run: | 488 | curl --request PATCH \ 489 | --url https://api.github.com/repos/${{ github.repository }} \ 490 | --header 'authorization: Bearer ${{ secrets.REPOSITORY_TOKEN }}' \ 491 | --header 'content-type: application/json' \ 492 | --data '{ 493 | "description":"${{ env.DOCKER_IMAGE_DESCRIPTION }}", 494 | "homepage":"", 495 | "has_issues":true, 496 | "has_discussions":true, 497 | "has_projects":false, 498 | "has_wiki":false 499 | }' \ 500 | --fail 501 | 502 | 503 | # ╔═════════════════════════════════════════════════════╗ 504 | # ║ ║ 505 | # ║ ║ 506 | # ║ MERGE IMAGES INTO SINGLE MANIFEST ║ 507 | # ║ ║ 508 | # ║ ║ 509 | # ╚═════════════════════════════════════════════════════╝ 510 | merge_platform_images: 511 | needs: docker 512 | if: needs.docker.outputs.WORKFLOW_BUILD == 'true' 513 | name: merge platform images to a single manifest 514 | runs-on: ubuntu-latest 515 | strategy: 516 | fail-fast: false 517 | matrix: 518 | registry: [docker.io, ghcr.io, quay.io] 519 | 520 | env: 521 | DOCKER_IMAGE_NAME: ${{ needs.docker.outputs.DOCKER_IMAGE_NAME }} 522 | DOCKER_IMAGE_MERGE_TAGS: ${{ needs.docker.outputs.DOCKER_IMAGE_MERGE_TAGS }} 523 | 524 | permissions: 525 | contents: read 526 | packages: write 527 | attestations: write 528 | id-token: write 529 | 530 | steps: 531 | # ╔═════════════════════════════════════════════════════╗ 532 | # ║ CONTAINER REGISTRY LOGIN ║ 533 | # ╚═════════════════════════════════════════════════════╝ 534 | # DOCKER HUB 535 | - name: docker / login to hub 536 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 537 | with: 538 | username: 11notes 539 | password: ${{ secrets.DOCKER_TOKEN }} 540 | 541 | # GITHUB CONTAINER REGISTRY 542 | - name: github / login to ghcr 543 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 544 | with: 545 | registry: ghcr.io 546 | username: 11notes 547 | password: ${{ secrets.GITHUB_TOKEN }} 548 | 549 | # REDHAT QUAY 550 | - name: quay / login to quay 551 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 552 | with: 553 | registry: quay.io 554 | username: 11notes+github 555 | password: ${{ secrets.QUAY_TOKEN }} 556 | 557 | 558 | # ╔═════════════════════════════════════════════════════╗ 559 | # ║ MERGE PLATFORM IMAGES MANIFEST ║ 560 | # ╚═════════════════════════════════════════════════════╝ 561 | # DOWNLOAD DIGESTS 562 | - name: platform merge / digest 563 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 564 | with: 565 | path: ${{ runner.temp }}/digests 566 | pattern: digests-* 567 | merge-multiple: true 568 | 569 | # SETUP BUILDX BUILDER 570 | - name: platform merge / buildx 571 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 572 | 573 | # GET META DATA 574 | - name: platform merge / meta 575 | id: meta 576 | uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 577 | with: 578 | images: ${{ matrix.registry }}/${{ env.DOCKER_IMAGE_NAME }} 579 | tags: | 580 | ${{ env.DOCKER_IMAGE_MERGE_TAGS }} 581 | 582 | # CREATE MANIFEST 583 | - name: platform merge / create manifest and push 584 | working-directory: ${{ runner.temp }}/digests 585 | run: | 586 | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 587 | $(printf 'docker.io/${{ env.DOCKER_IMAGE_NAME }}@sha256:%s ' *) 588 | 589 | # INSPECT MANIFEST 590 | - name: platform merge / inspect 591 | run: | 592 | docker buildx imagetools inspect ${{ matrix.registry }}/${{ env.DOCKER_IMAGE_NAME }}:${{ steps.meta.outputs.version }} --------------------------------------------------------------------------------