├── .gitattributes ├── .dockerignore ├── img └── RedisInsight.png ├── .gitignore ├── .github └── workflows │ ├── tags.yml │ ├── org.readme.yml │ ├── org.version.yml │ ├── cron.yml │ ├── org.update.yml │ └── docker.yml ├── .json ├── LICENSE ├── compose.yml ├── project.md ├── compose.secrets.yml ├── arch.dockerfile └── README.md /.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/ -------------------------------------------------------------------------------- /img/RedisInsight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11notes/docker-redis/HEAD/img/RedisInsight.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # default 2 | maintain/ 3 | node_modules/ 4 | 5 | # custom 6 | .env 7 | redis_password.txt -------------------------------------------------------------------------------- /.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 container 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" }' -------------------------------------------------------------------------------- /.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "11notes/redis", 3 | "name": "redis", 4 | "root": "/redis", 5 | "semver": { 6 | "version": "8.4.0" 7 | }, 8 | "readme": { 9 | "description": "run redis rootless and distroless", 10 | "introduction": "For developers, who are building real-time data-driven applications, Redis is the preferred, fastest, and most feature-rich cache, data structure server, and document and vector query engine.", 11 | "built": { 12 | "redis/redis": "https://github.com/redis/redis" 13 | }, 14 | "distroless": { 15 | "layers": [ 16 | "11notes/distroless" 17 | ] 18 | }, 19 | "comparison": { 20 | "image": "redis:8.4.0" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/org.readme.yml: -------------------------------------------------------------------------------- 1 | name: org.readme 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | # ╔═════════════════════════════════════════════════════╗ 8 | # ║ CREATE README.md ║ 9 | # ╚═════════════════════════════════════════════════════╝ 10 | readme: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: create README.md 14 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 15 | with: 16 | wait-for-completion: false 17 | workflow: docker.yml 18 | token: "${{ secrets.REPOSITORY_TOKEN }}" 19 | inputs: '{ "build":"false", "release":"false", "readme":"true", "run-name":"readme" }' -------------------------------------------------------------------------------- /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/org.version.yml: -------------------------------------------------------------------------------- 1 | name: org.version 2 | run-name: org.version ${{ inputs.version }} 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | version: 8 | description: 'set version for build' 9 | type: string 10 | required: true 11 | 12 | jobs: 13 | # ╔═════════════════════════════════════════════════════╗ 14 | # ║ BUILD VERSION {N} IMAGE ║ 15 | # ╚═════════════════════════════════════════════════════╝ 16 | version: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: setup config 20 | uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 21 | with: 22 | script: | 23 | const { Buffer } = require('node:buffer'); 24 | const etc = { 25 | version:"${{ github.event.inputs.version }}", 26 | semver:{disable:{rolling:true}} 27 | }; 28 | core.exportVariable('WORKFLOW_BASE64JSON', Buffer.from(JSON.stringify(etc)).toString('base64')); 29 | 30 | - name: build container image 31 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 32 | with: 33 | wait-for-completion: false 34 | workflow: docker.yml 35 | token: "${{ secrets.REPOSITORY_TOKEN }}" 36 | inputs: '{ "release":"false", "readme":"false", "etc":"${{ env.WORKFLOW_BASE64JSON }}" }' -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: cron 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 5 * * *" 7 | 8 | jobs: 9 | update: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | actions: read 14 | contents: write 15 | 16 | steps: 17 | - name: checkout all tags 18 | uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 19 | with: 20 | ref: 'master' 21 | fetch-depth: 0 22 | 23 | - name: get latest version and last tag 24 | run: | 25 | export LATEST_VERSION=$(curl -s https://api.github.com/repos/redis/redis/releases/latest | jq -r '.tag_name' | sed 's|v||') 26 | export LATEST_TAG=$(git describe --abbrev=0 --tags `git rev-list --tags --max-count=1` | sed 's|v||') 27 | echo "WORKFLOW_UPDATE_BASE64JSON=$(echo '{"version":"'${LATEST_VERSION}'", "tag":"'${LATEST_TAG}'"}' | base64)" >> "${GITHUB_ENV}" 28 | 29 | - name: call org.update 30 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 31 | with: 32 | wait-for-completion: false 33 | workflow: org.update.yml 34 | token: "${{ secrets.REPOSITORY_TOKEN }}" 35 | inputs: '{ "etc":"${{ env.WORKFLOW_UPDATE_BASE64JSON }}" }' 36 | 37 | update-v7: 38 | runs-on: ubuntu-latest 39 | 40 | permissions: 41 | actions: read 42 | contents: write 43 | 44 | steps: 45 | - name: get latest version of branch v7 46 | run: | 47 | VERSION="null" 48 | for PAGE in {1..10}; do 49 | V7=$(curl -s https://api.github.com/repos/redis/redis/tags?page=${PAGE} | jq -r ['.[].name | select(test("^7")) '][0] | sed 's/v//') 50 | if [ "${V7}" != "null" ]; then 51 | if ! curl -kILs --fail https://hub.docker.com/v2/repositories/11notes/redis/tags/${V7}; then 52 | echo "found version ${V7} not build yet" 53 | VERSION="${V7}" 54 | fi 55 | break 56 | fi 57 | done 58 | echo "LATEST_VERSION_V7=${VERSION}" >> "${GITHUB_ENV}" 59 | 60 | - name: cron-update / build docker image 61 | if: env.LATEST_VERSION_V7 != 'null' 62 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 63 | with: 64 | workflow: org.version.yml 65 | wait-for-completion: false 66 | token: "${{ secrets.REPOSITORY_TOKEN }}" 67 | inputs: '{ "version":"${{ env.LATEST_VERSION_V7 }}" }' -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | name: "kv" 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 | x-image-redis: &image 11 | image: "11notes/redis:8.4.0" 12 | <<: *lockdown 13 | 14 | services: 15 | redis: 16 | <<: *image 17 | environment: 18 | REDIS_PASSWORD: "${REDIS_PASSWORD}" 19 | TZ: "Europe/Zurich" 20 | networks: 21 | backend: 22 | volumes: 23 | - "redis.etc:/redis/etc" 24 | - "redis.var:/redis/var" 25 | tmpfs: 26 | - "/run:uid=1000,gid=1000" 27 | restart: "always" 28 | 29 | # start a replica of redis 30 | replica: 31 | <<: *image 32 | environment: 33 | REDIS_PASSWORD: "${REDIS_PASSWORD}" 34 | TZ: "Europe/Zurich" 35 | command: "--replica redis" 36 | networks: 37 | backend: 38 | volumes: 39 | - "replica.etc:/redis/etc" 40 | - "replica.var:/redis/var" 41 | tmpfs: 42 | - "/run:uid=1000,gid=1000" 43 | restart: "always" 44 | 45 | # start Redis only in-memory 46 | in-memory: 47 | <<: *image 48 | environment: 49 | REDIS_PASSWORD: "${REDIS_PASSWORD}" 50 | TZ: "Europe/Zurich" 51 | command: "--in-memory" 52 | networks: 53 | backend: 54 | volumes: 55 | - "in-memory.etc:/redis/etc" 56 | tmpfs: 57 | - "/run:uid=1000,gid=1000" 58 | restart: "always" 59 | 60 | # execute CLI commands via redis-cli 61 | cli: 62 | <<: *image 63 | depends_on: 64 | redis: 65 | condition: "service_healthy" 66 | restart: true 67 | environment: 68 | REDIS_HOST: "redis" 69 | REDIS_PASSWORD: "${REDIS_PASSWORD}" 70 | TZ: "Europe/Zurich" 71 | # start redis in cmd mode 72 | entrypoint: ["/usr/local/bin/redis", "--cmd"] 73 | # commands to execute in order 74 | command: 75 | - PING 76 | - --version 77 | - SET key value NX 78 | - GET key 79 | networks: 80 | backend: 81 | 82 | # demo container to actually view the databases 83 | gui: 84 | image: "redis/redisinsight" 85 | environment: 86 | RI_REDIS_HOST0: "redis" 87 | RI_REDIS_PASSWORD0: "${REDIS_PASSWORD}" 88 | RI_REDIS_HOST1: "replica" 89 | RI_REDIS_PASSWORD1: "${REDIS_PASSWORD}" 90 | RI_REDIS_HOST2: "in-memory" 91 | RI_REDIS_PASSWORD2: "${REDIS_PASSWORD}" 92 | TZ: "Europe/Zurich" 93 | ports: 94 | - "3000:5540/tcp" 95 | networks: 96 | backend: 97 | frontend: 98 | 99 | volumes: 100 | redis.etc: 101 | redis.var: 102 | replica.etc: 103 | replica.var: 104 | in-memory.etc: 105 | 106 | networks: 107 | frontend: 108 | backend: 109 | internal: true -------------------------------------------------------------------------------- /project.md: -------------------------------------------------------------------------------- 1 | ${{ image: RedisInsight.png }} 2 | 3 | ${{ content_synopsis }} This image will run redis [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 more security. Besides being more secure and slim than most images, it also offers additional start parameters to either start Redis in command mode, as a replica or as a in-memory database that persists nothing to disk. Simply provide the command needed: 4 | 5 | # COMMANDS 📟 6 | * **--cmd** - Will execute all commands against the Redis database specified via ```REDIS_HOST``` environment variable 7 | * **--replica MASTER** - Will start as replica from MASTER (can be IP, FQDN or container DNS) 8 | * **--in-memory** - Will start Redis only in memory 9 | * **[^1]** - ... and more? 10 | 11 | ${{ content_uvp }} Good question! Because ... 12 | 13 | ${{ github:> [!IMPORTANT] }} 14 | ${{ github:> }}* ... this image runs [rootless](https://github.com/11notes/RTFM/blob/main/linux/container/image/rootless.md) as 1000:1000 15 | ${{ github:> }}* ... this image has no shell since it is [distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md) 16 | ${{ github:> }}* ... this image is auto updated to the latest version via CI/CD 17 | ${{ github:> }}* ... this image has a health check 18 | ${{ github:> }}* ... this image runs read-only 19 | ${{ github:> }}* ... this image is automatically scanned for CVEs before and after publishing 20 | ${{ github:> }}* ... this image is created via a secure and pinned CI/CD process 21 | ${{ github:> }}* ... this image is very small 22 | ${{ github:> }}* ... this image can be used to execute commands after redis has started 23 | 24 | If you value security, simplicity and optimizations to the extreme, then this image might be for you. 25 | 26 | ${{ content_comparison }} 27 | 28 | ${{ title_volumes }} 29 | * **${{ json_root }}/etc** - Directory of your redis.conf 30 | * **${{ json_root }}/var** - Directory of your redis data 31 | 32 | ${{ content_compose }} 33 | 34 | ${{ content_defaults }} 35 | 36 | ${{ content_environment }} 37 | | `REDIS_PASSWORD` | Password used for authentication | | 38 | | `REDIS_PASSWORD_FILE` *(optional)* | Secrets file containing the password for authentication (check [compose.secrets.yml](https://github.com/11notes/docker-redis/blob/master/compose.secrets.yml)) | | 39 | | `REDIS_IP` *(optional)* | IP of Redis server | 0.0.0.0 | 40 | | `REDIS_PORT` *(optional)* | Port of Redis server | 6379 | 41 | | `REDIS_HOST` *(optional)* | IP of upstream Redis server when using ```--cmd``` | | 42 | | `REDISCLI_HISTFILE` *(optional)* | Disable history of redis-cli (for security) | /dev/null | 43 | 44 | ${{ content_source }} 45 | 46 | ${{ content_parent }} 47 | 48 | ${{ content_built }} 49 | 50 | ${{ content_tips }} 51 | 52 | [^1]: Sentinel mode will follow soon as well as the possibility to change the announce IP and port -------------------------------------------------------------------------------- /compose.secrets.yml: -------------------------------------------------------------------------------- 1 | name: "kv" 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 | x-image-redis: &image 11 | image: "11notes/redis:8.4.0" 12 | <<: *lockdown 13 | 14 | services: 15 | redis: 16 | <<: *image 17 | environment: 18 | REDIS_PASSWORD_FILE: "/run/secrets/redis_password" 19 | TZ: "Europe/Zurich" 20 | networks: 21 | backend: 22 | volumes: 23 | - "redis.etc:/redis/etc" 24 | - "redis.var:/redis/var" 25 | tmpfs: 26 | # needed for read-only 27 | - "/run:uid=1000,gid=1000" 28 | - "/run/secrets:uid=1000,gid=1000" 29 | secrets: 30 | - "redis_password" 31 | restart: "always" 32 | 33 | # start a replica of redis 34 | replica: 35 | <<: *image 36 | environment: 37 | REDIS_PASSWORD_FILE: "/run/secrets/redis_password" 38 | TZ: "Europe/Zurich" 39 | command: "--replica redis" 40 | networks: 41 | backend: 42 | volumes: 43 | - "replica.etc:/redis/etc" 44 | - "replica.var:/redis/var" 45 | tmpfs: 46 | # needed for read-only 47 | - "/run:uid=1000,gid=1000" 48 | - "/run/secrets:uid=1000,gid=1000" 49 | secrets: 50 | - "redis_password" 51 | restart: "always" 52 | 53 | # start Redis only in-memory 54 | in-memory: 55 | <<: *image 56 | environment: 57 | REDIS_PASSWORD_FILE: "/run/secrets/redis_password" 58 | TZ: "Europe/Zurich" 59 | command: "--in-memory" 60 | networks: 61 | backend: 62 | volumes: 63 | - "in-memory.etc:/redis/etc" 64 | tmpfs: 65 | - "/run:uid=1000,gid=1000" 66 | - "/run/secrets:uid=1000,gid=1000" 67 | secrets: 68 | - "redis_password" 69 | restart: "always" 70 | 71 | # execute CLI commands via redis-cli 72 | cli: 73 | <<: *image 74 | depends_on: 75 | redis: 76 | condition: "service_healthy" 77 | restart: true 78 | environment: 79 | REDIS_HOST: "redis" 80 | REDIS_PASSWORD_FILE: "/run/secrets/redis_password" 81 | TZ: "Europe/Zurich" 82 | # start redis in cmd mode 83 | entrypoint: ["/usr/local/bin/redis", "--cmd"] 84 | # commands to execute in order 85 | command: 86 | - PING 87 | - --version 88 | - SET key value NX 89 | - GET key 90 | networks: 91 | backend: 92 | tmpfs: 93 | - "/run/secrets:uid=1000,gid=1000" 94 | secrets: 95 | - "redis_password" 96 | 97 | # demo container to actually view the databases 98 | gui: 99 | image: "redis/redisinsight" 100 | environment: 101 | RI_REDIS_HOST0: "redis" 102 | RI_REDIS_PASSWORD0: "${REDIS_PASSWORD}" 103 | RI_REDIS_HOST1: "replica" 104 | RI_REDIS_PASSWORD1: "${REDIS_PASSWORD}" 105 | RI_REDIS_HOST2: "in-memory" 106 | RI_REDIS_PASSWORD2: "${REDIS_PASSWORD}" 107 | TZ: "Europe/Zurich" 108 | ports: 109 | - "3000:5540/tcp" 110 | networks: 111 | backend: 112 | frontend: 113 | 114 | volumes: 115 | redis.etc: 116 | redis.var: 117 | replica.etc: 118 | replica.var: 119 | in-memory.etc: 120 | 121 | networks: 122 | frontend: 123 | backend: 124 | internal: true 125 | 126 | secrets: 127 | redis_password: 128 | file: "./redis_password.txt" -------------------------------------------------------------------------------- /arch.dockerfile: -------------------------------------------------------------------------------- 1 | # ╔═════════════════════════════════════════════════════╗ 2 | # ║ SETUP ║ 3 | # ╚═════════════════════════════════════════════════════╝ 4 | # GLOBAL 5 | ARG APP_UID=1000 \ 6 | APP_GID=1000 \ 7 | BUILD_SRC=redis/redis.git \ 8 | BUILD_ROOT=/redis 9 | ARG BUILD_BIN=${BUILD_ROOT}/src/redis-server 10 | 11 | # :: FOREIGN IMAGES 12 | FROM 11notes/distroless AS distroless 13 | FROM 11notes/util:bin AS util-bin 14 | 15 | 16 | # ╔═════════════════════════════════════════════════════╗ 17 | # ║ BUILD ║ 18 | # ╚═════════════════════════════════════════════════════╝ 19 | # :: REDIS 20 | FROM alpine AS build 21 | COPY --from=util-bin / / 22 | ARG TARGETARCH \ 23 | TARGETVARIANT \ 24 | APP_VERSION \ 25 | APP_ROOT \ 26 | BUILD_SRC \ 27 | BUILD_ROOT \ 28 | BUILD_BIN \ 29 | BUILD_TLS=yes \ 30 | OPTIMIZATION=-O2 \ 31 | USE_JEMALLOC=yes 32 | 33 | RUN set -ex; \ 34 | apk add --update --no-cache \ 35 | git \ 36 | coreutils \ 37 | dpkg-dev dpkg \ 38 | g++ \ 39 | linux-headers \ 40 | make \ 41 | musl-dev \ 42 | openssl-dev \ 43 | openssl-libs-static \ 44 | jemalloc-dev \ 45 | libstdc++-dev \ 46 | xxhash-dev \ 47 | tcl \ 48 | procps; 49 | 50 | RUN set -ex; \ 51 | eleven git clone ${BUILD_SRC} ${APP_VERSION}; 52 | 53 | RUN set -ex; \ 54 | grep -E '^ *createBoolConfig[(]"protected-mode",.*, *1 *,.*[)],$' ${BUILD_ROOT}/src/config.c; \ 55 | sed -ri 's!^( *createBoolConfig[(]"protected-mode",.*, *)1( *,.*[)],)$!\10\2!' ${BUILD_ROOT}/src/config.c; \ 56 | grep -E '^ *createBoolConfig[(]"protected-mode",.*, *0 *,.*[)],$' ${BUILD_ROOT}/src/config.c; \ 57 | sed -i 's|$(REDIS_SERVER_NAME) $(REDIS_SENTINEL_NAME) $(REDIS_CLI_NAME) $(REDIS_BENCHMARK_NAME) $(REDIS_CHECK_RDB_NAME) $(REDIS_CHECK_AOF_NAME) $(TLS_MODULE) module_tests|$(REDIS_SERVER_NAME) $(REDIS_CLI_NAME) $(TLS_MODULE)|' ${BUILD_ROOT}/src/Makefile; 58 | 59 | RUN set -ex; \ 60 | if [ -d "${BUILD_ROOT}/deps/fast_float" ]; then \ 61 | cd ${BUILD_ROOT}/deps/fast_float; \ 62 | make -s -j $(nproc) \ 63 | LDFLAGS="--static"; \ 64 | fi; 65 | 66 | RUN set -ex; \ 67 | if [ -d "${BUILD_ROOT}/deps/xxhash" ]; then \ 68 | cd ${BUILD_ROOT}/deps/xxhash; \ 69 | sed -i 's|lib: libxxhash.a libxxhash|lib: libxxhash.a|g' Makefile; \ 70 | make lib -s -j $(nproc); \ 71 | fi; 72 | 73 | RUN set -ex; \ 74 | cd ${BUILD_ROOT}; \ 75 | make -s -j $(nproc) \ 76 | CFLAGS="${CFLAGS} -fPIC -static -static-libgcc -static-libstdc++" \ 77 | LDFLAGS="--static"; 78 | 79 | RUN set -ex; \ 80 | eleven distroless ${BUILD_BIN}; \ 81 | eleven distroless ${BUILD_ROOT}/src/redis-cli; \ 82 | mkdir -p /distroless${APP_ROOT}/etc; \ 83 | cp ${BUILD_ROOT}/redis.conf /distroless${APP_ROOT}/etc; 84 | 85 | RUN set -ex; \ 86 | eleven mkdir /distroless${APP_ROOT}/{etc,var}; \ 87 | sed -i 's/^# requirepass.*/requirepass \$REDIS_PASSWORD/' /distroless${APP_ROOT}/etc/redis.conf; \ 88 | sed -i 's/^# masterauth.*/masterauth \$REDIS_PASSWORD/' /distroless${APP_ROOT}/etc/redis.conf; \ 89 | sed -i 's@^pidfile.*@pidfile /run/redis.pid@' /distroless${APP_ROOT}/etc/redis.conf; \ 90 | sed -i 's@^dir.*@dir '${APP_ROOT}'/var@' /distroless${APP_ROOT}/etc/redis.conf; \ 91 | sed -i 's/^protected-mode.*/protected-mode no/' /distroless${APP_ROOT}/etc/redis.conf; \ 92 | sed -i 's/^bind.*/bind \$REDIS_IP/' /distroless${APP_ROOT}/etc/redis.conf; \ 93 | sed -i 's/^port.*/port \$REDIS_PORT/' /distroless${APP_ROOT}/etc/redis.conf; \ 94 | sed -i 's/^appendonly.*/appendonly yes/' /distroless${APP_ROOT}/etc/redis.conf; \ 95 | sed -i 's/^# save 3600.*/save 3600 1 300 100 60 10000/' /distroless${APP_ROOT}/etc/redis.conf; \ 96 | sed -i 's/^# shutdown-on-sigint.*/shutdown-on-sigint save/' /distroless${APP_ROOT}/etc/redis.conf; \ 97 | sed -i 's/^# shutdown-on-sigterm.*/shutdown-on-sigterm save/' /distroless${APP_ROOT}/etc/redis.conf; 98 | 99 | RUN set -ex; \ 100 | sed -i 's/^#.*//' /distroless${APP_ROOT}/etc/redis.conf; \ 101 | sed -i '/^$/d' /distroless${APP_ROOT}/etc/redis.conf; 102 | 103 | # INIT 104 | FROM 11notes/go:1.25 AS init 105 | COPY ./build / 106 | ARG APP_VERSION \ 107 | BUILD_ROOT=/go/redis 108 | ARG BUILD_BIN=${BUILD_ROOT}/redis 109 | 110 | RUN set -ex; \ 111 | cd ${BUILD_ROOT}; \ 112 | eleven go build ${BUILD_BIN} main.go; \ 113 | eleven distroless ${BUILD_BIN}; 114 | 115 | 116 | # ╔═════════════════════════════════════════════════════╗ 117 | # ║ IMAGE ║ 118 | # ╚═════════════════════════════════════════════════════╝ 119 | # :: HEADER 120 | FROM scratch 121 | 122 | # :: default arguments 123 | ARG TARGETPLATFORM \ 124 | TARGETOS \ 125 | TARGETARCH \ 126 | TARGETVARIANT \ 127 | APP_IMAGE \ 128 | APP_NAME \ 129 | APP_VERSION \ 130 | APP_ROOT \ 131 | APP_UID \ 132 | APP_GID \ 133 | APP_NO_CACHE 134 | 135 | # :: default environment 136 | ENV APP_IMAGE=${APP_IMAGE} \ 137 | APP_NAME=${APP_NAME} \ 138 | APP_VERSION=${APP_VERSION} \ 139 | APP_ROOT=${APP_ROOT} 140 | 141 | # :: app specific defaults 142 | ENV REDISCLI_HISTFILE=/dev/null \ 143 | REDIS_IP=0.0.0.0 \ 144 | REDIS_PORT=6379 145 | 146 | # :: multi-stage 147 | COPY --from=distroless / / 148 | COPY --from=build --chown=${APP_UID}:${APP_GID} /distroless/ / 149 | COPY --from=init /distroless/ / 150 | 151 | # :: HEALTH 152 | HEALTHCHECK --interval=5s --timeout=2s --start-period=5s \ 153 | CMD ["/usr/local/bin/redis-cli", "ping"] 154 | 155 | # :: EXECUTE 156 | USER ${APP_UID}:${APP_GID} 157 | ENTRYPOINT ["/usr/local/bin/redis"] -------------------------------------------------------------------------------- /.github/workflows/org.update.yml: -------------------------------------------------------------------------------- 1 | name: org.update 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | etc: 7 | description: 'base64 encoded json string' 8 | required: true 9 | 10 | jobs: 11 | update: 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | actions: read 16 | contents: write 17 | 18 | steps: 19 | - name: init / checkout 20 | uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 21 | with: 22 | ref: 'master' 23 | fetch-depth: 0 24 | 25 | - name: update / setup node 26 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 27 | with: 28 | node-version: '20' 29 | - run: npm i semver 30 | 31 | - name: update / compare latest with current version 32 | uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 33 | with: 34 | script: | 35 | (async()=>{ 36 | const { Buffer } = require('node:buffer'); 37 | const { inspect } = require('node:util'); 38 | const { existsSync, readFileSync, writeFileSync } = require('node:fs'); 39 | const { resolve } = require('node:path'); 40 | const semver = require('semver') 41 | 42 | // defaults 43 | const json = `${{ toJSON(github.event.inputs) }}`; 44 | const job = {inputs:{}, json:{}}; 45 | 46 | // check if inputs is valid base64 encoded json 47 | try{ 48 | if(json.length > 0){ 49 | const n = JSON.parse(json); 50 | if(n?.etc){ 51 | try{ 52 | job.inputs = JSON.parse(Buffer.from(n.etc, 'base64').toString('ascii')); 53 | if(!job.inputs?.version){ 54 | core.setFailed(`input does not contain valid semver version: ${inspect(job.inputs, {showHidden:false, depth:null, colors:true})}`); 55 | }else if(!job.inputs?.tag){ 56 | core.setFailed(`input does not contain valid git tag: ${inspect(job.inputs, {showHidden:false, depth:null, colors:true})}`); 57 | } 58 | }catch(e){ 59 | core.setFailed(`could not parse github.event.inputs.etc: ${n.etc} (${Buffer.from(n.etc, 'base64').toString('ascii')})`); 60 | } 61 | } 62 | } 63 | }catch(e){ 64 | core.setFailed(`could not parse github.event.inputs: ${json}`); 65 | } 66 | 67 | // check if .json exists 68 | try{ 69 | const path = resolve('.json'); 70 | if(existsSync(path)){ 71 | try{ 72 | job.json = JSON.parse(readFileSync(path).toString()); 73 | }catch(e){ 74 | throw new Error('could not parse .json'); 75 | } 76 | }else{ 77 | throw new Error('.json does not exist!'); 78 | } 79 | }catch(e){ 80 | core.setFailed(e); 81 | } 82 | 83 | // semver 84 | const latest = semver.valid(semver.coerce(job.inputs.version)); 85 | const current = semver.valid(semver.coerce(job.json.semver.version)); 86 | const tag = semver.valid(semver.coerce(job.inputs.tag)); 87 | const checks = {latestTagExists:true}; 88 | 89 | try{ 90 | const tag = await fetch(`https://hub.docker.com/v2/repositories/${job.json.image}/tags/${latest}`); 91 | if(tag.status === 404){ 92 | checks.latestTagExists = false; 93 | } 94 | }catch(e){ 95 | core.warning(e); 96 | } 97 | 98 | // compare 99 | if((latest && latest !== current) || !checks.latestTagExists){ 100 | core.info(`new ${semver.diff(current, latest)} release found (${latest}), updating ...`) 101 | job.json.semver.version = latest; 102 | 103 | // check if app has a build version 104 | if(job.inputs?.build){ 105 | job.json.build.args.version_build = job.inputs.build; 106 | } 107 | 108 | // update .json 109 | try{ 110 | writeFileSync(resolve('.json'), JSON.stringify(job.json, null, 2)); 111 | 112 | // export variables 113 | core.exportVariable('WORKFLOW_UPDATE', true); 114 | if(job.inputs?.unraid){ 115 | core.exportVariable('WORKFLOW_UPDATE_UNRAID', 'true'); 116 | core.exportVariable('WORKFLOW_UPDATE_UNRAID_BASE64JSON', Buffer.from(JSON.stringify({semversuffix:"unraid", uid:99, gid:100})).toString('base64')); 117 | } 118 | core.exportVariable('LATEST_TAG', semver.inc(tag, semver.diff(current, latest))); 119 | core.exportVariable('LATEST_VERSION', latest); 120 | if(job.inputs?.build) core.exportVariable('LATEST_BUILD', job.inputs.build); 121 | }catch(e){ 122 | core.setFailed(e); 123 | } 124 | }else{ 125 | core.info('no update required') 126 | } 127 | 128 | core.info(inspect(job, {showHidden:false, depth:null, colors:true})); 129 | })(); 130 | 131 | 132 | 133 | - name: update / checkout 134 | id: checkout 135 | if: env.WORKFLOW_UPDATE == 'true' 136 | run: | 137 | git config user.name "github-actions[bot]" 138 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 139 | git add .json 140 | git commit -m "chore: auto upgrade to v${{ env.LATEST_VERSION }}" 141 | git push origin HEAD:master 142 | 143 | - name: update / tag 144 | if: env.WORKFLOW_UPDATE == 'true' && steps.checkout.outcome == 'success' 145 | run: | 146 | SHA256=$(git rev-list --branches --max-count=1) 147 | git tag -a v${{ env.LATEST_TAG }} -m "v${{ env.LATEST_TAG }}" ${SHA256} 148 | git push --follow-tags 149 | 150 | - name: update / build container image 151 | id: build 152 | if: env.WORKFLOW_UPDATE == 'true' && steps.checkout.outcome == 'success' 153 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 154 | with: 155 | workflow: docker.yml 156 | wait-for-completion: false 157 | token: "${{ secrets.REPOSITORY_TOKEN }}" 158 | inputs: '{ "release":"true", "readme":"true", "run-name":"update v${{ env.LATEST_VERSION }}" }' 159 | ref: "v${{ env.LATEST_TAG }}" 160 | 161 | - name: update / build container image for unraid 162 | if: env.WORKFLOW_UPDATE_UNRAID == 'true' && steps.checkout.outcome == 'success' && steps.build.outcome == 'success' 163 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 164 | with: 165 | workflow: docker.yml 166 | wait-for-completion: false 167 | token: "${{ secrets.REPOSITORY_TOKEN }}" 168 | inputs: '{ "release":"false", "readme":"false", "run-name":"update unraid v${{ env.LATEST_VERSION }}", "etc":"${{ env.WORKFLOW_UPDATE_UNRAID_BASE64JSON }}" }' 169 | ref: "v${{ env.LATEST_TAG }}" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![banner](https://raw.githubusercontent.com/11notes/static/refs/heads/main/img/banner/README.png) 2 | 3 | # REDIS 4 | ![size](https://img.shields.io/badge/image_size-19MB-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/redis?color=2b75d6)![5px](https://raw.githubusercontent.com/11notes/static/refs/heads/main/img/markdown/transparent5x2px.png)[](https://github.com/11notes/docker-redis/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 redis rootless and distroless 7 | 8 | # INTRODUCTION 📢 9 | 10 | For developers, who are building real-time data-driven applications, Redis is the preferred, fastest, and most feature-rich cache, data structure server, and document and vector query engine. 11 | 12 | ![REDISINSIGHT](https://github.com/11notes/docker-redis/blob/master/img/RedisInsight.png?raw=true) 13 | 14 | # SYNOPSIS 📖 15 | **What can I do with this?** This image will run redis [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 more security. Besides being more secure and slim than most images, it also offers additional start parameters to either start Redis in command mode, as a replica or as a in-memory database that persists nothing to disk. Simply provide the command needed: 16 | 17 | # COMMANDS 📟 18 | * **--cmd** - Will execute all commands against the Redis database specified via ```REDIS_HOST``` environment variable 19 | * **--replica MASTER** - Will start as replica from MASTER (can be IP, FQDN or container DNS) 20 | * **--in-memory** - Will start Redis only in memory 21 | * **[^1]** - ... and more? 22 | 23 | # UNIQUE VALUE PROPOSITION 💶 24 | **Why should I run this image and not the other image(s) that already exist?** Good question! Because ... 25 | 26 | > [!IMPORTANT] 27 | >* ... this image runs [rootless](https://github.com/11notes/RTFM/blob/main/linux/container/image/rootless.md) as 1000:1000 28 | >* ... this image has no shell since it is [distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md) 29 | >* ... this image is auto updated to the latest version via CI/CD 30 | >* ... this image has a health check 31 | >* ... this image runs read-only 32 | >* ... this image is automatically scanned for CVEs before and after publishing 33 | >* ... this image is created via a secure and pinned CI/CD process 34 | >* ... this image is very small 35 | >* ... this image can be used to execute commands after redis has started 36 | 37 | If you value security, simplicity and optimizations to the extreme, then this image might be for you. 38 | 39 | # COMPARISON 🏁 40 | Below you find a comparison between this image and the most used or original one. 41 | 42 | | **image** | **size on disk** | **init default as** | **[distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md)** | supported architectures 43 | | ---: | ---: | :---: | :---: | :---: | 44 | | 11notes/redis | 19MB | 1000:1000 | ✅ | amd64, arm64, armv7 | 45 | | redis | 139MB | 0:0 | ❌ | 386, amd64, arm64v8, armv5, armv7, mips64le, ppc64le, s390x | 46 | 47 | # VOLUMES 📁 48 | * **/redis/etc** - Directory of your redis.conf 49 | * **/redis/var** - Directory of your redis data 50 | 51 | # COMPOSE ✂️ 52 | Checkout [compose.secrets.yml](https://github.com/11notes/docker-redis/blob/master/compose.secrets.yml) if you want to use secrets instead of environment variables. 53 | ```yaml 54 | name: "kv" 55 | 56 | x-lockdown: &lockdown 57 | # prevents write access to the image itself 58 | read_only: true 59 | # prevents any process within the container to gain more privileges 60 | security_opt: 61 | - "no-new-privileges=true" 62 | 63 | x-image-redis: &image 64 | image: "11notes/redis:8.4.0" 65 | <<: *lockdown 66 | 67 | services: 68 | redis: 69 | <<: *image 70 | environment: 71 | REDIS_PASSWORD: "${REDIS_PASSWORD}" 72 | TZ: "Europe/Zurich" 73 | networks: 74 | backend: 75 | volumes: 76 | - "redis.etc:/redis/etc" 77 | - "redis.var:/redis/var" 78 | tmpfs: 79 | - "/run:uid=1000,gid=1000" 80 | restart: "always" 81 | 82 | # start a replica of redis 83 | replica: 84 | <<: *image 85 | environment: 86 | REDIS_PASSWORD: "${REDIS_PASSWORD}" 87 | TZ: "Europe/Zurich" 88 | command: "--replica redis" 89 | networks: 90 | backend: 91 | volumes: 92 | - "replica.etc:/redis/etc" 93 | - "replica.var:/redis/var" 94 | tmpfs: 95 | - "/run:uid=1000,gid=1000" 96 | restart: "always" 97 | 98 | # start Redis only in-memory 99 | in-memory: 100 | <<: *image 101 | environment: 102 | REDIS_PASSWORD: "${REDIS_PASSWORD}" 103 | TZ: "Europe/Zurich" 104 | command: "--in-memory" 105 | networks: 106 | backend: 107 | volumes: 108 | - "in-memory.etc:/redis/etc" 109 | tmpfs: 110 | - "/run:uid=1000,gid=1000" 111 | restart: "always" 112 | 113 | # execute CLI commands via redis-cli 114 | cli: 115 | <<: *image 116 | depends_on: 117 | redis: 118 | condition: "service_healthy" 119 | restart: true 120 | environment: 121 | REDIS_HOST: "redis" 122 | REDIS_PASSWORD: "${REDIS_PASSWORD}" 123 | TZ: "Europe/Zurich" 124 | # start redis in cmd mode 125 | entrypoint: ["/usr/local/bin/redis", "--cmd"] 126 | # commands to execute in order 127 | command: 128 | - PING 129 | - --version 130 | - SET key value NX 131 | - GET key 132 | networks: 133 | backend: 134 | 135 | # demo container to actually view the databases 136 | gui: 137 | image: "redis/redisinsight" 138 | environment: 139 | RI_REDIS_HOST0: "redis" 140 | RI_REDIS_PASSWORD0: "${REDIS_PASSWORD}" 141 | RI_REDIS_HOST1: "replica" 142 | RI_REDIS_PASSWORD1: "${REDIS_PASSWORD}" 143 | RI_REDIS_HOST2: "in-memory" 144 | RI_REDIS_PASSWORD2: "${REDIS_PASSWORD}" 145 | TZ: "Europe/Zurich" 146 | ports: 147 | - "3000:5540/tcp" 148 | networks: 149 | backend: 150 | frontend: 151 | 152 | volumes: 153 | redis.etc: 154 | redis.var: 155 | replica.etc: 156 | replica.var: 157 | in-memory.etc: 158 | 159 | networks: 160 | frontend: 161 | backend: 162 | internal: true 163 | ``` 164 | 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). 165 | 166 | # DEFAULT SETTINGS 🗃️ 167 | | Parameter | Value | Description | 168 | | --- | --- | --- | 169 | | `user` | docker | user name | 170 | | `uid` | 1000 | [user identifier](https://en.wikipedia.org/wiki/User_identifier) | 171 | | `gid` | 1000 | [group identifier](https://en.wikipedia.org/wiki/Group_identifier) | 172 | | `home` | /redis | home directory of user docker | 173 | 174 | # ENVIRONMENT 📝 175 | | Parameter | Value | Default | 176 | | --- | --- | --- | 177 | | `TZ` | [Time Zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | | 178 | | `DEBUG` | Will activate debug option for container image and app (if available) | | 179 | | `REDIS_PASSWORD` | Password used for authentication | | 180 | | `REDIS_PASSWORD_FILE` *(optional)* | Secrets file containing the password for authentication (check [compose.secrets.yml](https://github.com/11notes/docker-redis/blob/master/compose.secrets.yml)) | | 181 | | `REDIS_IP` *(optional)* | IP of Redis server | 0.0.0.0 | 182 | | `REDIS_PORT` *(optional)* | Port of Redis server | 6379 | 183 | | `REDIS_HOST` *(optional)* | IP of upstream Redis server when using ```--cmd``` | | 184 | | `REDISCLI_HISTFILE` *(optional)* | Disable history of redis-cli (for security) | /dev/null | 185 | 186 | # MAIN TAGS 🏷️ 187 | These are the main tags for the image. There is also a tag for each commit and its shorthand sha256 value. 188 | 189 | * [8.4.0](https://hub.docker.com/r/11notes/redis/tags?name=8.4.0) 190 | 191 | ### There is no latest tag, what am I supposed to do about updates? 192 | 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 ```:8.4.0``` you can use ```:8``` or ```:8.4```. 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. 193 | 194 | 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! 195 | 196 | # REGISTRIES ☁️ 197 | ``` 198 | docker pull 11notes/redis:8.4.0 199 | docker pull ghcr.io/11notes/redis:8.4.0 200 | docker pull quay.io/11notes/redis:8.4.0 201 | ``` 202 | 203 | # SOURCE 💾 204 | * [11notes/redis](https://github.com/11notes/docker-redis) 205 | 206 | # PARENT IMAGE 🏛️ 207 | > [!IMPORTANT] 208 | >This image is not based on another image but uses [scratch](https://hub.docker.com/_/scratch) as the starting layer. 209 | >The image consists of the following distroless layers that were added: 210 | >* [11notes/distroless](https://github.com/11notes/docker-distroless/blob/master/arch.dockerfile) - contains users, timezones and Root CA certificates, nothing else 211 | 212 | # BUILT WITH 🧰 213 | * [redis/redis](https://github.com/redis/redis) 214 | 215 | # GENERAL TIPS 📌 216 | > [!TIP] 217 | >* Use a reverse proxy like Traefik, Nginx, HAproxy to terminate TLS and to protect your endpoints 218 | >* Use Let’s Encrypt DNS-01 challenge to obtain valid SSL certificates for your services 219 | 220 | [^1]: Sentinel mode will follow soon as well as the possibility to change the announce IP and port 221 | 222 | # ElevenNotes™️ 223 | 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-redis/releases) for breaking changes. If you have any problems with using this image simply raise an [issue](https://github.com/11notes/docker-redis/issues), thanks. If you have a question or inputs please create a new [discussion](https://github.com/11notes/docker-redis/discussions) instead of an issue. You can find all my other repositories on [github](https://github.com/11notes?tab=repositories). 224 | 225 | *created 03.12.2025, 21:54:00 (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 | # CHECKOUT REPOSITORY 54 | - name: init / checkout 55 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 56 | with: 57 | ref: ${{ github.ref_name }} 58 | 59 | - name: matrix / setup list 60 | id: setup-matrix 61 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 62 | with: 63 | script: | 64 | const { existsSync, readFileSync } = require('node:fs'); 65 | const { inspect } = require('node:util'); 66 | const { resolve } = require('node:path'); 67 | const opt = {dot:{}}; 68 | 69 | try{ 70 | const path = resolve('.json'); 71 | if(existsSync(path)){ 72 | try{ 73 | opt.dot = JSON.parse(readFileSync(path).toString()); 74 | }catch(e){ 75 | throw new Error('could not parse .json'); 76 | } 77 | }else{ 78 | throw new Error('.json does not exist'); 79 | } 80 | }catch(e){ 81 | core.setFailed(e); 82 | } 83 | 84 | const platforms = ( 85 | ("${{ github.event.inputs.platform }}" != "amd64,arm64,arm/v7") ? "${{ github.event.inputs.platform }}".split(",") : ( 86 | (opt.dot?.platform) ? opt.dot.platform.split(",") : "${{ github.event.inputs.platform }}".split(",") 87 | ) 88 | ); 89 | 90 | const matrix = {include:[]}; 91 | if("${{ github.event.inputs.readme }}" === "true" && "${{ github.event.inputs.build }}" === "false"){ 92 | matrix.include.push({platform:"amd64", runner:"ubuntu-24.04"}); 93 | }else{ 94 | for(const platform of platforms){ 95 | switch(platform){ 96 | case "amd64": matrix.include.push({platform:platform, runner:"ubuntu-24.04"}); break; 97 | case "arm64": matrix.include.push({platform:platform, runner:"ubuntu-24.04-arm"}); break; 98 | case "arm/v7": matrix.include.push({platform:platform, runner:"ubuntu-24.04-arm"}); break; 99 | } 100 | } 101 | } 102 | 103 | const stringify = JSON.stringify(matrix); 104 | core.setOutput('stringify', stringify); 105 | 106 | // print 107 | core.info(inspect({opt:opt, matrix:matrix, platforms:platforms}, {showHidden:false, depth:null, colors:true})); 108 | 109 | 110 | # ╔═════════════════════════════════════════════════════╗ 111 | # ║ ║ 112 | # ║ ║ 113 | # ║ BUILD CONTAINER IMAGE ║ 114 | # ║ ║ 115 | # ║ ║ 116 | # ╚═════════════════════════════════════════════════════╝ 117 | docker: 118 | name: create container image 119 | runs-on: ${{ matrix.runner }} 120 | strategy: 121 | fail-fast: false 122 | matrix: ${{ fromJSON(needs.matrix.outputs.stringify) }} 123 | outputs: 124 | DOCKER_IMAGE_NAME: ${{ steps.setup-environment.outputs.DOCKER_IMAGE_NAME }} 125 | DOCKER_IMAGE_MERGE_TAGS: ${{ steps.setup-environment.outputs.DOCKER_IMAGE_MERGE_TAGS }} 126 | DOCKER_IMAGE_DESCRIPTION: ${{ steps.setup-environment.outputs.DOCKER_IMAGE_DESCRIPTION }} 127 | DOCKER_IMAGE_NAME_AND_VERSION: ${{ steps.setup-environment.outputs.DOCKER_IMAGE_NAME_AND_VERSION }} 128 | DOCKER_IMAGE_ARGUMENTS: ${{ steps.setup-environment.outputs.DOCKER_IMAGE_ARGUMENTS }} 129 | WORKFLOW_BUILD: ${{ steps.setup-environment.outputs.WORKFLOW_BUILD }} 130 | 131 | timeout-minutes: 1440 132 | 133 | services: 134 | registry: 135 | image: registry:2 136 | ports: 137 | - 5000:5000 138 | 139 | permissions: 140 | actions: write 141 | contents: write 142 | packages: write 143 | attestations: write 144 | id-token: write 145 | security-events: write 146 | 147 | needs: matrix 148 | 149 | steps: 150 | # ╔═════════════════════════════════════════════════════╗ 151 | # ║ SETUP ENVIRONMENT ║ 152 | # ╚═════════════════════════════════════════════════════╝ 153 | # CHECKOUT ALL DEPTHS (ALL TAGS) 154 | - name: init / checkout 155 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 156 | with: 157 | ref: ${{ github.ref_name }} 158 | fetch-depth: 0 159 | 160 | # SETUP ENVIRONMENT VARIABLES AND INPUTS 161 | - name: init / setup environment 162 | id: setup-environment 163 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 164 | with: 165 | script: | 166 | const { existsSync, readFileSync } = require('node:fs'); 167 | const { resolve } = require('node:path'); 168 | const { inspect } = require('node:util'); 169 | const { Buffer } = require('node:buffer'); 170 | const inputs = `${{ toJSON(github.event.inputs) }}`. 171 | replace(/"platform":\s*"\[(.+)\]",/i, `"platform": [$1],`); 172 | const opt = {input:{}, dot:{}}; 173 | 174 | try{ 175 | if(inputs.length > 0){ 176 | opt.input = JSON.parse(inputs); 177 | if(opt.input?.etc){ 178 | opt.input.etc = JSON.parse(Buffer.from(opt.input.etc, 'base64').toString('ascii')); 179 | } 180 | } 181 | }catch(e){ 182 | core.warning('could not parse github.event.inputs'); 183 | core.warning(inputs); 184 | } 185 | 186 | try{ 187 | const path = resolve('.json'); 188 | if(existsSync(path)){ 189 | try{ 190 | opt.dot = JSON.parse(readFileSync(path).toString()); 191 | }catch(e){ 192 | throw new Error('could not parse .json'); 193 | } 194 | }else{ 195 | throw new Error('.json does not exist'); 196 | } 197 | }catch(e){ 198 | core.setFailed(e); 199 | } 200 | 201 | const docker = { 202 | image:{ 203 | name:opt.dot.image, 204 | arch:(opt.input?.etc?.arch || opt.dot?.arch || 'linux/amd64,linux/arm64'), 205 | prefix:((opt.input?.etc?.semverprefix) ? `${opt.input?.etc?.semverprefix}-` : ''), 206 | suffix:((opt.input?.etc?.semversuffix) ? `-${opt.input?.etc?.semversuffix}` : ''), 207 | description:(opt.dot?.readme?.description || ''), 208 | platform:{ 209 | sanitized:"${{ matrix.platform }}".replace(/[^A-Z-a-z0-9]+/i, ""), 210 | }, 211 | tags:[], 212 | build:(opt.input?.build === undefined) ? false : opt.input.build, 213 | }, 214 | app:{ 215 | image:opt.dot.image, 216 | name:opt.dot.name, 217 | version:(opt.input?.etc?.version || opt.dot?.semver?.version), 218 | root:opt.dot.root, 219 | UID:(opt.input?.etc?.uid || 1000), 220 | GID:(opt.input?.etc?.gid || 1000), 221 | no_cache:new Date().getTime(), 222 | }, 223 | cache:{ 224 | registry:'localhost:5000/', 225 | enable:(opt.input?.etc?.cache === undefined) ? true : opt.input.etc.cache, 226 | }, 227 | tags:[], 228 | merge_tags:[], 229 | }; 230 | 231 | docker.cache.name = `${docker.image.name}:${docker.image.prefix}buildcache${docker.image.suffix}`; 232 | docker.cache.grype = `${docker.cache.registry}${docker.image.name}:${docker.image.prefix}grype${docker.image.suffix}`; 233 | docker.app.prefix = docker.image.prefix; 234 | docker.app.suffix = docker.image.suffix; 235 | 236 | const semver = docker.app.version.split('.'); 237 | // setup tags 238 | if(!opt.dot?.semver?.disable?.rolling && !opt.input.etc?.semver?.disable?.rolling){ 239 | docker.image.tags.push('rolling'); 240 | } 241 | if(opt.input?.etc?.dockerfile !== 'arch.dockerfile' && opt.input?.etc?.tag){ 242 | docker.image.tags.push(opt.input.etc.tag); 243 | if(Array.isArray(semver)){ 244 | if(semver.length >= 1) docker.image.tags.push(`${opt.input.etc.tag}-${semver[0]}`); 245 | if(semver.length >= 2) docker.image.tags.push(`${opt.input.etc.tag}-${semver[0]}.${semver[1]}`); 246 | if(semver.length >= 3) docker.image.tags.push(`${opt.input.etc.tag}-${semver[0]}.${semver[1]}.${semver[2]}`); 247 | }else{ 248 | docker.image.tags.push(`${opt.input.etc.tag}-${docker.app.version}`); 249 | } 250 | docker.cache.name = `${docker.image.name}:buildcache-${opt.input.etc.tag}`; 251 | }else if(docker.app.version !== 'latest'){ 252 | if(Array.isArray(semver)){ 253 | if(semver.length >= 1) docker.image.tags.push(`${semver[0]}`); 254 | if(semver.length >= 2) docker.image.tags.push(`${semver[0]}.${semver[1]}`); 255 | if(semver.length >= 3) docker.image.tags.push(`${semver[0]}.${semver[1]}.${semver[2]}`); 256 | } 257 | if(opt.dot?.semver?.stable && new RegExp(opt.dot?.semver.stable, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('stable'); 258 | if(opt.dot?.semver?.latest && new RegExp(opt.dot?.semver.latest, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('latest'); 259 | }else{ 260 | docker.image.tags.push('latest'); 261 | } 262 | 263 | for(const tag of docker.image.tags){ 264 | docker.tags.push(`${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}-${docker.image.platform.sanitized}`); 265 | docker.tags.push(`ghcr.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}-${docker.image.platform.sanitized}`); 266 | docker.tags.push(`quay.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}-${docker.image.platform.sanitized}`); 267 | docker.merge_tags.push(`${docker.image.prefix}${tag}${docker.image.suffix}`); 268 | } 269 | 270 | // setup build arguments 271 | if(opt.input?.etc?.build?.args){ 272 | for(const arg in opt.input.etc.build.args){ 273 | docker.app[arg] = opt.input.etc.build.args[arg]; 274 | } 275 | } 276 | if(opt.dot?.build?.args){ 277 | for(const arg in opt.dot.build.args){ 278 | docker.app[arg] = opt.dot.build.args[arg]; 279 | } 280 | } 281 | const arguments = []; 282 | for(const argument in docker.app){ 283 | arguments.push(`APP_${argument.toUpperCase()}=${docker.app[argument]}`); 284 | } 285 | 286 | // export to environment 287 | core.exportVariable('DOCKER_CACHE_REGISTRY', docker.cache.registry); 288 | core.exportVariable('DOCKER_CACHE_NAME', `${docker.cache.name}-${docker.image.platform.sanitized}`); 289 | core.exportVariable('DOCKER_CACHE_GRYPE', docker.cache.grype); 290 | 291 | core.exportVariable('DOCKER_IMAGE_NAME', docker.image.name); 292 | core.setOutput('DOCKER_IMAGE_NAME', docker.image.name); 293 | core.exportVariable('DOCKER_IMAGE_TAGS', docker.tags.join(',')); 294 | core.exportVariable('DOCKER_IMAGE_MERGE_TAGS', docker.merge_tags.join("\r\n")); 295 | core.setOutput('DOCKER_IMAGE_MERGE_TAGS', docker.merge_tags.join("\r\n")); 296 | core.exportVariable('DOCKER_IMAGE_DESCRIPTION', docker.image.description); 297 | core.setOutput('DOCKER_IMAGE_DESCRIPTION', docker.image.description); 298 | core.exportVariable('DOCKER_IMAGE_ARGUMENTS', arguments.join("\r\n")); 299 | core.setOutput('DOCKER_IMAGE_ARGUMENTS', arguments.join("\r\n")); 300 | core.exportVariable('DOCKER_IMAGE_DOCKERFILE', opt.input?.etc?.dockerfile || 'arch.dockerfile'); 301 | core.exportVariable('DOCKER_IMAGE_PLATFORM_SANITIZED', docker.image.platform.sanitized); 302 | core.exportVariable('DOCKER_IMAGE_NAME_AND_VERSION', `${docker.image.name}:${docker.app.version}`); 303 | core.setOutput('DOCKER_IMAGE_NAME_AND_VERSION', `${docker.image.name}:${docker.app.version}`); 304 | 305 | core.setOutput('DOCKER_IMAGE_TAG_LATEST', ( 306 | docker.image.tags.includes('latest') ? "true" : "false" 307 | )); 308 | 309 | core.exportVariable('WORKFLOW_BUILD', docker.image.build); 310 | core.setOutput('WORKFLOW_BUILD', docker.image.build); 311 | core.exportVariable('WORKFLOW_BUILD_NO_CACHE', !docker.cache.enable); 312 | 313 | core.exportVariable('WORKFLOW_CREATE_RELEASE', (opt.input?.release === undefined) ? false : opt.input.release); 314 | core.exportVariable('WORKFLOW_CREATE_README', (opt.input?.readme === undefined) ? false : opt.input.readme); 315 | core.exportVariable('WORKFLOW_GRYPE_FAIL_ON_SEVERITY', (opt.dot?.grype?.fail === undefined) ? true : opt.dot.grype.fail); 316 | core.exportVariable('WORKFLOW_GRYPE_SEVERITY_CUTOFF', (opt.dot?.grype?.severity || 'critical')); 317 | 318 | // print 319 | core.info(inspect({opt:opt, docker:docker}, {showHidden:false, depth:null, colors:true})); 320 | 321 | 322 | # ╔═════════════════════════════════════════════════════╗ 323 | # ║ CONTAINER REGISTRY LOGIN ║ 324 | # ╚═════════════════════════════════════════════════════╝ 325 | # DOCKER HUB 326 | - name: docker / login to hub 327 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 328 | with: 329 | username: 11notes 330 | password: ${{ secrets.DOCKER_TOKEN }} 331 | 332 | # GITHUB CONTAINER REGISTRY 333 | - name: github / login to ghcr 334 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 335 | with: 336 | registry: ghcr.io 337 | username: 11notes 338 | password: ${{ secrets.GITHUB_TOKEN }} 339 | 340 | # REDHAT QUAY 341 | - name: quay / login to quay 342 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 343 | with: 344 | registry: quay.io 345 | username: 11notes+github 346 | password: ${{ secrets.QUAY_TOKEN }} 347 | 348 | 349 | # ╔═════════════════════════════════════════════════════╗ 350 | # ║ BUILD CONTAINER IMAGE ║ 351 | # ╚═════════════════════════════════════════════════════╝ 352 | # SETUP QEMU 353 | - name: container image / setup qemu 354 | if: env.WORKFLOW_BUILD == 'true' && matrix.platform == 'arm/v7' 355 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 356 | with: 357 | image: tonistiigi/binfmt:qemu-v8.1.5 358 | cache-image: false 359 | 360 | # SETUP BUILDX BUILDER WITH USING LOCAL REGISTRY 361 | - name: container image / setup buildx 362 | if: env.WORKFLOW_BUILD == 'true' 363 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 364 | with: 365 | driver-opts: network=host 366 | 367 | # BUILD CONTAINER IMAGE FROM GLOBAL CACHE (DOCKER HUB) AND PUSH TO LOCAL CACHE 368 | - name: container image / build 369 | if: env.WORKFLOW_BUILD == 'true' 370 | id: image-build 371 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 372 | with: 373 | context: . 374 | no-cache: ${{ env.WORKFLOW_BUILD_NO_CACHE }} 375 | file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} 376 | push: true 377 | platforms: linux/${{ matrix.platform }} 378 | cache-from: type=registry,ref=${{ env.DOCKER_CACHE_NAME }} 379 | cache-to: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true 380 | build-args: | 381 | ${{ env.DOCKER_IMAGE_ARGUMENTS }} 382 | tags: | 383 | ${{ env.DOCKER_CACHE_GRYPE }} 384 | 385 | # SCAN LOCAL CONTAINER IMAGE WITH GRYPE 386 | - name: container image / scan with grype 387 | if: env.WORKFLOW_BUILD == 'true' 388 | id: grype 389 | uses: anchore/scan-action@1638637db639e0ade3258b51db49a9a137574c3e # v6.5.1 390 | with: 391 | image: ${{ env.DOCKER_CACHE_GRYPE }} 392 | fail-build: ${{ env.WORKFLOW_GRYPE_FAIL_ON_SEVERITY }} 393 | severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} 394 | output-format: 'sarif' 395 | by-cve: true 396 | cache-db: true 397 | 398 | # OUTPUT CVE REPORT IF SCAN FAILS 399 | - name: container image / scan with grype FAILED 400 | if: env.WORKFLOW_BUILD == 'true' && (failure() || steps.grype.outcome == 'failure') && steps.image-build.outcome == 'success' 401 | uses: anchore/scan-action@1638637db639e0ade3258b51db49a9a137574c3e # v6.5.1 402 | with: 403 | image: ${{ env.DOCKER_CACHE_GRYPE }} 404 | fail-build: false 405 | severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} 406 | output-format: 'table' 407 | by-cve: true 408 | cache-db: true 409 | 410 | # PUSH IMAGE TO ALL REGISTRIES IF CLEAN 411 | - name: container image / push to registries 412 | id: image-push 413 | if: env.WORKFLOW_BUILD == 'true' 414 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 415 | with: 416 | context: . 417 | no-cache: ${{ env.WORKFLOW_BUILD_NO_CACHE }} 418 | file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} 419 | push: true 420 | sbom: true 421 | provenance: mode=max 422 | platforms: linux/${{ matrix.platform }} 423 | cache-from: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }} 424 | cache-to: type=registry,ref=${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true 425 | build-args: | 426 | ${{ env.DOCKER_IMAGE_ARGUMENTS }} 427 | tags: | 428 | ${{ env.DOCKER_IMAGE_TAGS }} 429 | 430 | # CREATE ATTESTATION ARTIFACTS 431 | - name: container image / create attestation artifacts 432 | if: env.WORKFLOW_BUILD == 'true' && steps.image-push.outcome == 'success' 433 | uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 434 | with: 435 | subject-name: docker.io/${{ env.DOCKER_IMAGE_NAME }} 436 | subject-digest: ${{ steps.image-push.outputs.digest }} 437 | push-to-registry: false 438 | 439 | # EXPORT DIGEST 440 | - name: container image / export digest 441 | if: env.WORKFLOW_BUILD == 'true' && steps.image-push.outcome == 'success' 442 | run: | 443 | mkdir -p ${{ runner.temp }}/digests 444 | digest="${{ steps.image-push.outputs.digest }}" 445 | touch "${{ runner.temp }}/digests/${digest#sha256:}" 446 | 447 | # UPLOAD DIGEST 448 | - name: container image / upload 449 | if: env.WORKFLOW_BUILD == 'true' && steps.image-push.outcome == 'success' 450 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 451 | with: 452 | name: digests-linux-${{ env.DOCKER_IMAGE_PLATFORM_SANITIZED }} 453 | path: ${{ runner.temp }}/digests/* 454 | if-no-files-found: error 455 | 456 | 457 | # ╔═════════════════════════════════════════════════════╗ 458 | # ║ CREATE GITHUB RELEASE ║ 459 | # ╚═════════════════════════════════════════════════════╝ 460 | # CREATE RELEASE MARKUP 461 | - name: github release / prepare markdown 462 | if: env.WORKFLOW_CREATE_RELEASE == 'true' && matrix.platform == 'amd64' 463 | id: git-release 464 | uses: 11notes/action-docker-release@v1 465 | 466 | # CREATE GITHUB RELEASE 467 | - name: github release / create 468 | if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-release.outcome == 'success' 469 | uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 470 | env: 471 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 472 | with: 473 | tag_name: ${{ github.ref }} 474 | release_name: ${{ github.ref }} 475 | body: ${{ steps.git-release.outputs.release }} 476 | draft: false 477 | prerelease: false 478 | 479 | 480 | # ╔═════════════════════════════════════════════════════╗ 481 | # ║ ║ 482 | # ║ ║ 483 | # ║ MERGE IMAGES INTO SINGLE MANIFEST ║ 484 | # ║ ║ 485 | # ║ ║ 486 | # ╚═════════════════════════════════════════════════════╝ 487 | merge_platform_images: 488 | needs: docker 489 | if: needs.docker.outputs.WORKFLOW_BUILD == 'true' 490 | name: merge platform images to a single manifest 491 | runs-on: ubuntu-latest 492 | strategy: 493 | fail-fast: false 494 | matrix: 495 | registry: [docker.io, ghcr.io, quay.io] 496 | 497 | env: 498 | DOCKER_IMAGE_NAME: ${{ needs.docker.outputs.DOCKER_IMAGE_NAME }} 499 | DOCKER_IMAGE_MERGE_TAGS: ${{ needs.docker.outputs.DOCKER_IMAGE_MERGE_TAGS }} 500 | 501 | permissions: 502 | contents: read 503 | packages: write 504 | attestations: write 505 | id-token: write 506 | 507 | steps: 508 | # ╔═════════════════════════════════════════════════════╗ 509 | # ║ CONTAINER REGISTRY LOGIN ║ 510 | # ╚═════════════════════════════════════════════════════╝ 511 | # DOCKER HUB 512 | - name: docker / login to hub 513 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 514 | with: 515 | username: 11notes 516 | password: ${{ secrets.DOCKER_TOKEN }} 517 | 518 | # GITHUB CONTAINER REGISTRY 519 | - name: github / login to ghcr 520 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 521 | with: 522 | registry: ghcr.io 523 | username: 11notes 524 | password: ${{ secrets.GITHUB_TOKEN }} 525 | 526 | # REDHAT QUAY 527 | - name: quay / login to quay 528 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 529 | with: 530 | registry: quay.io 531 | username: 11notes+github 532 | password: ${{ secrets.QUAY_TOKEN }} 533 | 534 | 535 | # ╔═════════════════════════════════════════════════════╗ 536 | # ║ MERGE PLATFORM IMAGES MANIFEST ║ 537 | # ╚═════════════════════════════════════════════════════╝ 538 | # DOWNLOAD DIGESTS 539 | - name: platform merge / digest 540 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 541 | with: 542 | path: ${{ runner.temp }}/digests 543 | pattern: digests-* 544 | merge-multiple: true 545 | 546 | # SETUP BUILDX BUILDER 547 | - name: platform merge / buildx 548 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 549 | 550 | # GET META DATA 551 | - name: platform merge / meta 552 | id: meta 553 | uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 554 | with: 555 | images: ${{ matrix.registry }}/${{ env.DOCKER_IMAGE_NAME }} 556 | tags: | 557 | ${{ env.DOCKER_IMAGE_MERGE_TAGS }} 558 | 559 | # CREATE MANIFEST 560 | - name: platform merge / create manifest and push 561 | working-directory: ${{ runner.temp }}/digests 562 | run: | 563 | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 564 | $(printf 'docker.io/${{ env.DOCKER_IMAGE_NAME }}@sha256:%s ' *) 565 | 566 | # INSPECT MANIFEST 567 | - name: platform merge / inspect 568 | run: | 569 | docker buildx imagetools inspect ${{ matrix.registry }}/${{ env.DOCKER_IMAGE_NAME }}:${{ steps.meta.outputs.version }} 570 | 571 | 572 | # ╔═════════════════════════════════════════════════════╗ 573 | # ║ ║ 574 | # ║ ║ 575 | # ║ FINALIZE IMAGE CREATION ║ 576 | # ║ ║ 577 | # ║ ║ 578 | # ╚═════════════════════════════════════════════════════╝ 579 | finally: 580 | if: ${{ always() }} 581 | needs: 582 | - docker 583 | - merge_platform_images 584 | name: finalize image creation 585 | runs-on: ubuntu-latest 586 | 587 | env: 588 | DOCKER_IMAGE_NAME: ${{ needs.docker.outputs.DOCKER_IMAGE_NAME }} 589 | DOCKER_IMAGE_DESCRIPTION: ${{ needs.docker.outputs.DOCKER_IMAGE_DESCRIPTION }} 590 | DOCKER_IMAGE_NAME_AND_VERSION: ${{ needs.docker.outputs.DOCKER_IMAGE_NAME_AND_VERSION }} 591 | DOCKER_IMAGE_ARGUMENTS: ${{ needs.docker.outputs.DOCKER_IMAGE_ARGUMENTS }} 592 | DOCKER_IMAGE_TAG_LATEST: ${{ needs.docker.outputs.DOCKER_IMAGE_TAG_LATEST }} 593 | 594 | permissions: 595 | contents: write 596 | 597 | steps: 598 | # ╔═════════════════════════════════════════════════════╗ 599 | # ║ SETUP ENVIRONMENT ║ 600 | # ╚═════════════════════════════════════════════════════╝ 601 | # CHECKOUT ALL DEPTHS (ALL TAGS) 602 | - name: init / checkout 603 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 604 | with: 605 | ref: master 606 | fetch-depth: 0 607 | 608 | # ╔═════════════════════════════════════════════════════╗ 609 | # ║ CONTAINER REGISTRY LOGIN ║ 610 | # ╚═════════════════════════════════════════════════════╝ 611 | # DOCKER HUB 612 | - name: docker / login to hub 613 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 614 | with: 615 | username: 11notes 616 | password: ${{ secrets.DOCKER_TOKEN }} 617 | 618 | # ╔═════════════════════════════════════════════════════╗ 619 | # ║ CREATE README.md ║ 620 | # ╚═════════════════════════════════════════════════════╝ 621 | # CHECKOUT HEAD TO BE UP TO DATE WITH EVERYTHING 622 | - name: README.md / checkout 623 | if: github.event.inputs.readme == 'true' 624 | continue-on-error: true 625 | run: | 626 | git checkout HEAD 627 | 628 | # CREATE MAKRDOWN OF README.md 629 | - name: README.md / create 630 | if: github.event.inputs.readme == 'true' 631 | id: github-readme 632 | continue-on-error: true 633 | uses: 11notes/action-docker-readme@v1 634 | 635 | # UPLOAD README.md to DOCKER HUB 636 | - name: README.md / push to Docker Hub 637 | if: github.event.inputs.readme == 'true' && steps.github-readme.outcome == 'success' && hashFiles('README_NONGITHUB.md') != '' 638 | continue-on-error: true 639 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 640 | env: 641 | DOCKER_USER: 11notes 642 | DOCKER_PASS: ${{ secrets.DOCKER_TOKEN }} 643 | with: 644 | destination_container_repo: ${{ env.DOCKER_IMAGE_NAME }} 645 | provider: dockerhub 646 | short_description: ${{ env.DOCKER_IMAGE_DESCRIPTION }} 647 | readme_file: 'README_NONGITHUB.md' 648 | 649 | # COMMIT NEW README.md, LICENSE and compose 650 | - name: README.md / github commit & push 651 | if: github.event.inputs.readme == 'true' && steps.github-readme.outcome == 'success' && hashFiles('README.md') != '' 652 | continue-on-error: true 653 | run: | 654 | git config user.name "github-actions[bot]" 655 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 656 | git add README.md 657 | if [ -f compose.yaml ]; then 658 | git add compose.yaml 659 | fi 660 | if [ -f compose.yml ]; then 661 | git add compose.yml 662 | fi 663 | if [ -f LICENSE ]; then 664 | git add LICENSE 665 | fi 666 | git commit -m "update README.md" 667 | git push origin HEAD:master 668 | 669 | # ╔═════════════════════════════════════════════════════╗ 670 | # ║ GITHUB REPOSITORY DEFAULT SETTINGS ║ 671 | # ╚═════════════════════════════════════════════════════╝ 672 | # UPDATE REPO WITH DEFAULT SETTINGS FOR CONTAINER IMAGE 673 | - name: github / update description and set repo defaults 674 | run: | 675 | curl --request PATCH \ 676 | --url https://api.github.com/repos/${{ github.repository }} \ 677 | --header 'authorization: Bearer ${{ secrets.REPOSITORY_TOKEN }}' \ 678 | --header 'content-type: application/json' \ 679 | --data '{ 680 | "description":"${{ env.DOCKER_IMAGE_DESCRIPTION }}", 681 | "homepage":"", 682 | "has_issues":true, 683 | "has_discussions":true, 684 | "has_projects":false, 685 | "has_wiki":false 686 | }' \ 687 | --fail 688 | 689 | # ╔═════════════════════════════════════════════════════╗ 690 | # ║ REMOVE DEFAULT IMAGE TAG IF PRESENT ║ 691 | # ╚═════════════════════════════════════════════════════╝ 692 | # REMOVE LATEST TAG FROM DOCKER HUB 693 | - name: docker hub / delete latest tag if needed 694 | if: env.DOCKER_IMAGE_TAG_LATEST == 'false' 695 | run: | 696 | TOKEN=$(curl --request POST \ 697 | -s \ 698 | --url https://hub.docker.com/v2/auth/token \ 699 | --header 'content-type: application/json' \ 700 | --data '{ 701 | "identifier":"11notes", 702 | "secret":"${{ secrets.DOCKER_TOKEN }}" 703 | }' \ 704 | --fail | jq -r ".access_token") 705 | curl -s -H "Authorization: Bearer ${TOKEN}" -X DELETE https://hub.docker.com/v2/repositories/${{ env.DOCKER_IMAGE_NAME }}/tags/latest | echo ":latest tag not found" --------------------------------------------------------------------------------