├── .gitignore ├── .gitattributes ├── .dockerignore ├── .github └── workflows │ ├── tags.yml │ ├── readme.yml │ ├── cve.yml │ ├── cron.update.yml │ └── docker.yml ├── rootfs └── caddy │ └── etc │ └── default.json ├── .json ├── compose.yml ├── LICENSE ├── project.md ├── arch.dockerfile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # default 2 | maintain/ 3 | node_modules/ 4 | .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/ 8 | .env -------------------------------------------------------------------------------- /.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" }' -------------------------------------------------------------------------------- /.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" }' -------------------------------------------------------------------------------- /rootfs/caddy/etc/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": { 3 | "http": { 4 | "servers": { 5 | "health": { 6 | "listen": ["127.0.0.1:3000"], 7 | "routes": [ 8 | { 9 | "handle": [{ 10 | "handler": "static_response", 11 | "status_code": 200 12 | }] 13 | } 14 | ] 15 | }, 16 | "demo": { 17 | "listen": [":80"], 18 | "routes": [ 19 | { 20 | "handle": [{ 21 | "handler": "static_response", 22 | "body": "11notes/caddy" 23 | }] 24 | } 25 | ] 26 | } 27 | } 28 | } 29 | }, 30 | "storage":{ 31 | "module": "file_system", 32 | "root": "/caddy/var" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "11notes/caddy", 3 | "name": "caddy", 4 | "root": "/caddy", 5 | "semver": { 6 | "version": "2.10.2" 7 | }, 8 | "readme": { 9 | "description": "Run caddy rootless and distroless.", 10 | "introduction": "Caddy is a web server written in Go, known for its simplicity and automatic HTTPS features. It acts as a powerful and flexible reverse proxy, handling various protocols like HTTP, HTTPS, WebSockets, gRPC, and FastCGI.", 11 | "built": { 12 | "caddy": "https://github.com/caddyserver/caddy" 13 | }, 14 | "distroless": { 15 | "layers": [ 16 | "11notes/distroless", 17 | "11notes/distroless:localhealth" 18 | ] 19 | }, 20 | "comparison": { 21 | "image": "caddy" 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | name: "proxy" 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 | caddy: 12 | image: "11notes/caddy:2.10.0" 13 | # use Caddyfile instead of json 14 | # command: ["run", "--config", "/caddy/etc/Caddyfile"] 15 | <<: *lockdown 16 | environment: 17 | TZ: "Europe/Zurich" 18 | ports: 19 | - "80:80/tcp" 20 | - "443:443/tcp" 21 | volumes: 22 | - "caddy.etc:/caddy/etc" 23 | - "caddy.var:/caddy/var" 24 | # optional volume (can be tmpfs instead) to store backups of your config 25 | - "caddy.backup:/caddy/backup" 26 | networks: 27 | frontend: 28 | sysctls: 29 | # allow rootless container to access port 80 and higher 30 | net.ipv4.ip_unprivileged_port_start: 80 31 | restart: "always" 32 | 33 | volumes: 34 | caddy.etc: 35 | caddy.var: 36 | caddy.backup: 37 | 38 | networks: 39 | frontend: -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /project.md: -------------------------------------------------------------------------------- 1 | ${{ content_synopsis }} This image will run caddy [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. This image will by default use the JSON format. If you don’t want that but you want to use the Caddyfile format, simply check the [compose.yml](https://github.com/11notes/docker-caddy/blob/master/compose.yml) for the command and make sure your Caddyfile contains at least these settings for the storage and health check to work: 2 | 3 | ``` 4 | { 5 | storage file_system /caddy/var 6 | } 7 | 127.0.0.1:3000 { 8 | respond / 200 9 | } 10 | ``` 11 | 12 | ${{ content_uvp }} Good question! Because ... 13 | 14 | ${{ github:> [!IMPORTANT] }} 15 | ${{ github:> }}* ... this image runs [rootless](https://github.com/11notes/RTFM/blob/main/linux/container/image/rootless.md) as 1000:1000 16 | ${{ github:> }}* ... this image has no shell since it is [distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md) 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 verifies all external payloads 22 | ${{ github:> }}* ... this image is very small 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_config }} 29 | ```json 30 | ${{ include: ./rootfs/caddy/etc/default.json }} 31 | ``` 32 | 33 | ${{ title_volumes }} 34 | * **${{ json_root }}/etc** - Directory of your default.json config 35 | * **${{ json_root }}/var** - Directory of all dynamic data 36 | 37 | ${{ content_compose }} 38 | 39 | ${{ content_defaults }} 40 | 41 | ${{ content_environment }} 42 | | `XDG_CONFIG_HOME` | Directory where to store backups of your config | /caddy/backup | 43 | 44 | ${{ content_source }} 45 | 46 | ${{ content_parent }} 47 | 48 | ${{ content_built }} 49 | 50 | ${{ content_tips }} 51 | 52 | ${{ title_caution }} 53 | ${{ github:> [!CAUTION] }} 54 | ${{ github:> }}* Don’t forget to add the ```127.0.0.1:3000``` listen directive to your config with a HTTP 200 status code for the default health check or create your own! 55 | ${{ github:> }}* The default.json config has a server listening on HTTP, don’t do that. Normally redirect HTTP to HTTPS. There are exceptions[^1] for HTTP use. 56 | 57 | [^1]: Some OTA or other services only work via HTTP (unencrypted) since adding all the Root CA would not fit into their limited memory. -------------------------------------------------------------------------------- /arch.dockerfile: -------------------------------------------------------------------------------- 1 | # ╔═════════════════════════════════════════════════════╗ 2 | # ║ SETUP ║ 3 | # ╚═════════════════════════════════════════════════════╝ 4 | # GLOBAL 5 | ARG APP_UID=1000 \ 6 | APP_GID=1000 \ 7 | BUILD_SRC=caddyserver/caddy.git \ 8 | BUILD_BIN=/caddy \ 9 | BUILD_ROOT=/go/caddy/cmd/caddy 10 | 11 | # :: FOREIGN IMAGES 12 | FROM 11notes/distroless AS distroless 13 | FROM 11notes/distroless:localhealth AS distroless-localhealth 14 | FROM 11notes/util AS util 15 | 16 | # ╔═════════════════════════════════════════════════════╗ 17 | # ║ BUILD ║ 18 | # ╚═════════════════════════════════════════════════════╝ 19 | # :: CADDY 20 | FROM 11notes/go:1.24 AS build 21 | ARG APP_VERSION \ 22 | BUILD_SRC \ 23 | BUILD_ROOT \ 24 | BUILD_BIN \ 25 | TARGETARCH \ 26 | TARGETPLATFORM \ 27 | TARGETVARIANT 28 | 29 | RUN set -ex; \ 30 | eleven git clone ${BUILD_SRC} v${APP_VERSION}; 31 | 32 | RUN set -ex; \ 33 | cd ${BUILD_ROOT}; \ 34 | eleven go build ${BUILD_BIN} main.go; 35 | 36 | RUN set -ex; \ 37 | eleven distroless ${BUILD_BIN}; 38 | 39 | # :: FILE-SYSTEM 40 | FROM alpine AS file-system 41 | COPY --from=util / / 42 | ARG APP_ROOT 43 | 44 | RUN set -ex; \ 45 | eleven mkdir /distroless/caddy/{etc,var,backup} 46 | 47 | 48 | # ╔═════════════════════════════════════════════════════╗ 49 | # ║ IMAGE ║ 50 | # ╚═════════════════════════════════════════════════════╝ 51 | # :: HEADER 52 | FROM scratch 53 | 54 | # :: default arguments 55 | ARG TARGETPLATFORM \ 56 | TARGETOS \ 57 | TARGETARCH \ 58 | TARGETVARIANT \ 59 | APP_IMAGE \ 60 | APP_NAME \ 61 | APP_VERSION \ 62 | APP_ROOT \ 63 | APP_UID \ 64 | APP_GID \ 65 | APP_NO_CACHE 66 | 67 | # :: default environment 68 | ENV APP_IMAGE=${APP_IMAGE} \ 69 | APP_NAME=${APP_NAME} \ 70 | APP_VERSION=${APP_VERSION} \ 71 | APP_ROOT=${APP_ROOT} 72 | 73 | # :: app specific environment 74 | ENV XDG_CONFIG_HOME="/caddy/backup" 75 | 76 | # :: multi-stage 77 | COPY --from=distroless / / 78 | COPY --from=distroless-localhealth / / 79 | COPY --from=build /distroless/ / 80 | COPY --from=file-system --chown=${APP_UID}:${APP_GID} /distroless/ / 81 | COPY --chown=${APP_UID}:${APP_GID} ./rootfs/ / 82 | 83 | # :: PERSISTENT DATA 84 | VOLUME ["${APP_ROOT}/etc", "${APP_ROOT}/var"] 85 | 86 | # :: MONITORING 87 | HEALTHCHECK --interval=5s --timeout=2s --start-period=5s \ 88 | CMD ["/usr/local/bin/localhealth", "http://127.0.0.1:3000/", "-I"] 89 | 90 | # :: EXECUTE 91 | USER ${APP_UID}:${APP_GID} 92 | ENTRYPOINT ["/usr/local/bin/caddy"] 93 | CMD ["run", "--config", "/caddy/etc/default.json"] -------------------------------------------------------------------------------- /.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 -sSL https://api.github.com/repos/caddyserver/caddy/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 | if(repository.dot?.readme?.comparison?.image){ 75 | repository.dot.readme.comparison.image = repository.dot.readme.comparison.image.replace(current, repository.dot.semver.version); 76 | } 77 | 78 | try{ 79 | writeFileSync(resolve('.json'), JSON.stringify(repository.dot, null, 2)); 80 | core.exportVariable('WORKFLOW_AUTO_UPDATE', true); 81 | }catch(e){ 82 | core.setFailed(e); 83 | } 84 | }else{ 85 | core.info('no new release found'); 86 | } 87 | 88 | core.info(inspect(repository.dot, {showHidden:false, depth:null, colors:true})); 89 | 90 | - name: cron-update / checkout 91 | id: checkout 92 | if: env.WORKFLOW_AUTO_UPDATE == 'true' 93 | run: | 94 | git config user.name "github-actions[bot]" 95 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 96 | git add .json 97 | git commit -m "[upgrade] ${{ env.LATEST_VERSION }}" 98 | git push origin HEAD:master 99 | 100 | - name: cron-update / tag 101 | if: env.WORKFLOW_AUTO_UPDATE == 'true' && steps.checkout.outcome == 'success' 102 | run: | 103 | SHA256=$(git rev-list --branches --max-count=1) 104 | git tag -a v${{ env.WORKFLOW_NEW_TAG }} -m "v${{ env.WORKFLOW_NEW_TAG }}" ${SHA256} 105 | git push --follow-tags 106 | 107 | - name: cron-update / build docker image 108 | if: env.WORKFLOW_AUTO_UPDATE == 'true' && steps.checkout.outcome == 'success' 109 | uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 110 | with: 111 | workflow: docker.yml 112 | wait-for-completion: false 113 | token: "${{ secrets.REPOSITORY_TOKEN }}" 114 | inputs: '{ "release":"true", "readme":"true" }' 115 | ref: "v${{ env.WORKFLOW_NEW_TAG }}" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![banner](https://github.com/11notes/defaults/blob/main/static/img/banner.png?raw=true) 2 | 3 | # CADDY 4 | ![size](https://img.shields.io/docker/image-size/11notes/caddy/2.10.0?color=0eb305)![5px](https://github.com/11notes/defaults/blob/main/static/img/transparent5x2px.png?raw=true)![version](https://img.shields.io/docker/v/11notes/caddy/2.10.0?color=eb7a09)![5px](https://github.com/11notes/defaults/blob/main/static/img/transparent5x2px.png?raw=true)![pulls](https://img.shields.io/docker/pulls/11notes/caddy?color=2b75d6)![5px](https://github.com/11notes/defaults/blob/main/static/img/transparent5x2px.png?raw=true)[](https://github.com/11notes/docker-CADDY/issues)![5px](https://github.com/11notes/defaults/blob/main/static/img/transparent5x2px.png?raw=true)![swiss_made](https://img.shields.io/badge/Swiss_Made-FFFFFF?labelColor=FF0000&logo=data:image/svg%2bxml;base64,PHN2ZyB2ZXJzaW9uPSIxIiB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDMyIDMyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0idHJhbnNwYXJlbnQiLz4KICA8cGF0aCBkPSJtMTMgNmg2djdoN3Y2aC03djdoLTZ2LTdoLTd2LTZoN3oiIGZpbGw9IiNmZmYiLz4KPC9zdmc+) 5 | 6 | Run caddy rootless and distroless. 7 | 8 | # INTRODUCTION 📢 9 | 10 | Caddy is a web server written in Go, known for its simplicity and automatic HTTPS features. It acts as a powerful and flexible reverse proxy, handling various protocols like HTTP, HTTPS, WebSockets, gRPC, and FastCGI. 11 | 12 | # SYNOPSIS 📖 13 | **What can I do with this?** This image will run caddy [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. This image will by default use the JSON format. If you don’t want that but you want to use the Caddyfile format, simply check the [compose.yml](https://github.com/11notes/docker-caddy/blob/master/compose.yml) for the command and make sure your Caddyfile contains at least these settings for the storage and health check to work: 14 | 15 | ``` 16 | { 17 | storage file_system /caddy/var 18 | } 19 | 127.0.0.1:3000 { 20 | respond / 200 21 | } 22 | ``` 23 | 24 | # UNIQUE VALUE PROPOSITION 💶 25 | **Why should I run this image and not the other image(s) that already exist?** Good question! Because ... 26 | 27 | > [!IMPORTANT] 28 | >* ... this image runs [rootless](https://github.com/11notes/RTFM/blob/main/linux/container/image/rootless.md) as 1000:1000 29 | >* ... this image has no shell since it is [distroless](https://github.com/11notes/RTFM/blob/main/linux/container/image/distroless.md) 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 verifies all external payloads 35 | >* ... this image is very small 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** | 11notes/caddy:2.10.0 | caddy | 43 | | ---: | :---: | :---: | 44 | | **image size on disk** | 14.4MB | 50.5MB | 45 | | **process UID/GID** | 1000/1000 | 0/0 | 46 | | **distroless?** | ✅ | ❌ | 47 | | **rootless?** | ✅ | ❌ | 48 | 49 | 50 | # DEFAULT CONFIG 📑 51 | ```json 52 | { 53 | "apps": { 54 | "http": { 55 | "servers": { 56 | "health": { 57 | "listen": ["127.0.0.1:3000"], 58 | "routes": [ 59 | { 60 | "handle": [{ 61 | "handler": "static_response", 62 | "status_code": 200 63 | }] 64 | } 65 | ] 66 | }, 67 | "demo": { 68 | "listen": [":80"], 69 | "routes": [ 70 | { 71 | "handle": [{ 72 | "handler": "static_response", 73 | "body": "11notes/caddy" 74 | }] 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | }, 81 | "storage":{ 82 | "module": "file_system", 83 | "root": "/caddy/var" 84 | } 85 | } 86 | 87 | ``` 88 | 89 | # VOLUMES 📁 90 | * **/caddy/etc** - Directory of your default.json config 91 | * **/caddy/var** - Directory of all dynamic data 92 | 93 | # COMPOSE ✂️ 94 | ```yaml 95 | name: "proxy" 96 | 97 | x-lockdown: &lockdown 98 | # prevents write access to the image itself 99 | read_only: true 100 | # prevents any process within the container to gain more privileges 101 | security_opt: 102 | - "no-new-privileges=true" 103 | 104 | services: 105 | caddy: 106 | image: "11notes/caddy:2.10.0" 107 | # use Caddyfile instead of json 108 | # command: ["run", "--config", "/caddy/etc/Caddyfile"] 109 | <<: *lockdown 110 | environment: 111 | TZ: "Europe/Zurich" 112 | ports: 113 | - "80:80/tcp" 114 | - "443:443/tcp" 115 | volumes: 116 | - "caddy.etc:/caddy/etc" 117 | - "caddy.var:/caddy/var" 118 | # optional volume (can be tmpfs instead) to store backups of your config 119 | - "caddy.backup:/caddy/backup" 120 | networks: 121 | frontend: 122 | sysctls: 123 | # allow rootless container to access port 80 and higher 124 | net.ipv4.ip_unprivileged_port_start: 80 125 | restart: "always" 126 | 127 | volumes: 128 | caddy.etc: 129 | caddy.var: 130 | caddy.backup: 131 | 132 | networks: 133 | frontend: 134 | ``` 135 | 136 | # DEFAULT SETTINGS 🗃️ 137 | | Parameter | Value | Description | 138 | | --- | --- | --- | 139 | | `user` | docker | user name | 140 | | `uid` | 1000 | [user identifier](https://en.wikipedia.org/wiki/User_identifier) | 141 | | `gid` | 1000 | [group identifier](https://en.wikipedia.org/wiki/Group_identifier) | 142 | | `home` | /caddy | home directory of user docker | 143 | 144 | # ENVIRONMENT 📝 145 | | Parameter | Value | Default | 146 | | --- | --- | --- | 147 | | `TZ` | [Time Zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | | 148 | | `DEBUG` | Will activate debug option for container image and app (if available) | | 149 | | `XDG_CONFIG_HOME` | Directory where to store backups of your config | /caddy/backup | 150 | 151 | # MAIN TAGS 🏷️ 152 | These are the main tags for the image. There is also a tag for each commit and its shorthand sha256 value. 153 | 154 | * [2.10.0](https://hub.docker.com/r/11notes/caddy/tags?name=2.10.0) 155 | 156 | ### There is no latest tag, what am I supposed to do about updates? 157 | It is of my opinion that the ```:latest``` tag is dangerous. Many times, I’ve introduced **breaking** changes to my images. This would have messed up everything for some people. 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 ```:2.10.0``` you can use ```:2``` or ```:2.10```. 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. 158 | 159 | 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! 160 | 161 | # REGISTRIES ☁️ 162 | ``` 163 | docker pull 11notes/caddy:2.10.0 164 | docker pull ghcr.io/11notes/caddy:2.10.0 165 | docker pull quay.io/11notes/caddy:2.10.0 166 | ``` 167 | 168 | # SOURCE 💾 169 | * [11notes/caddy](https://github.com/11notes/docker-CADDY) 170 | 171 | # PARENT IMAGE 🏛️ 172 | > [!IMPORTANT] 173 | >This image is not based on another image but uses [scratch](https://hub.docker.com/_/scratch) as the starting layer. 174 | >The image consists of the following distroless layers that were added: 175 | >* [11notes/distroless](https://github.com/11notes/docker-distroless/blob/master/arch.dockerfile) - contains users, timezones and Root CA certificates 176 | >* [11notes/distroless:localhealth](https://github.com/11notes/docker-distroless/blob/master/localhealth.dockerfile) - app to execute HTTP requests only on 127.0.0.1 177 | 178 | # BUILT WITH 🧰 179 | * [caddy](https://github.com/caddyserver/caddy) 180 | 181 | # GENERAL TIPS 📌 182 | > [!TIP] 183 | >* Use a reverse proxy like Traefik, Nginx, HAproxy to terminate TLS and to protect your endpoints 184 | >* Use Let’s Encrypt DNS-01 challenge to obtain valid SSL certificates for your services 185 | 186 | # CAUTION ⚠️ 187 | > [!CAUTION] 188 | >* Don’t forget to add the ```127.0.0.1:3000``` listen directive to your config with a HTTP 200 status code for the default health check or create your own! 189 | >* The default.json config has a server listening on HTTP, don’t do that. Normally redirect HTTP to HTTPS. There are exceptions[^1] for HTTP use. 190 | 191 | [^1]: Some OTA or other services only work via HTTP (unencrypted) since adding all the Root CA would not fit into their limited memory. 192 | 193 | # ElevenNotes™️ 194 | 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-caddy/releases) for breaking changes. If you have any problems with using this image simply raise an [issue](https://github.com/11notes/docker-caddy/issues), thanks. If you have a question or inputs please create a new [discussion](https://github.com/11notes/docker-caddy/discussions) instead of an issue. You can find all my other repositories on [github](https://github.com/11notes?tab=repositories). 195 | 196 | *created 12.08.2025, 00:27:08 (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 | runs-on: 14 | description: 'set runs-on for workflow (github or selfhosted)' 15 | type: string 16 | required: false 17 | default: 'ubuntu-22.04' 18 | 19 | 20 | build: 21 | description: 'set WORKFLOW_BUILD' 22 | required: false 23 | default: 'true' 24 | 25 | release: 26 | description: 'set WORKFLOW_GITHUB_RELEASE' 27 | required: false 28 | default: 'false' 29 | 30 | readme: 31 | description: 'set WORKFLOW_GITHUB_README' 32 | required: false 33 | default: 'false' 34 | 35 | etc: 36 | description: 'base64 encoded json string' 37 | required: false 38 | 39 | jobs: 40 | docker: 41 | runs-on: ${{ inputs.runs-on }} 42 | timeout-minutes: 1440 43 | 44 | services: 45 | registry: 46 | image: registry:2 47 | ports: 48 | - 5000:5000 49 | 50 | permissions: 51 | actions: read 52 | contents: write 53 | packages: write 54 | 55 | steps: 56 | - name: init / checkout 57 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 58 | with: 59 | ref: ${{ github.ref_name }} 60 | fetch-depth: 0 61 | 62 | - name: init / setup environment 63 | uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 64 | with: 65 | script: | 66 | const { existsSync, readFileSync } = require('node:fs'); 67 | const { resolve } = require('node:path'); 68 | const { inspect } = require('node:util'); 69 | const { Buffer } = require('node:buffer'); 70 | const inputs = `${{ toJSON(github.event.inputs) }}`; 71 | const opt = {input:{}, dot:{}}; 72 | 73 | try{ 74 | if(inputs.length > 0){ 75 | opt.input = JSON.parse(inputs); 76 | if(opt.input?.etc){ 77 | opt.input.etc = JSON.parse(Buffer.from(opt.input.etc, 'base64').toString('ascii')); 78 | } 79 | } 80 | }catch(e){ 81 | core.warning('could not parse github.event.inputs'); 82 | } 83 | 84 | try{ 85 | const path = resolve('.json'); 86 | if(existsSync(path)){ 87 | try{ 88 | opt.dot = JSON.parse(readFileSync(path).toString()); 89 | }catch(e){ 90 | throw new Error('could not parse .json'); 91 | } 92 | }else{ 93 | throw new Error('.json does not exist'); 94 | } 95 | }catch(e){ 96 | core.setFailed(e); 97 | } 98 | 99 | core.info(inspect(opt, {showHidden:false, depth:null, colors:true})); 100 | 101 | const docker = { 102 | image:{ 103 | name:opt.dot.image, 104 | arch:(opt.input?.etc?.arch || opt.dot?.arch || 'linux/amd64,linux/arm64'), 105 | prefix:((opt.input?.etc?.semverprefix) ? `${opt.input?.etc?.semverprefix}-` : ''), 106 | suffix:((opt.input?.etc?.semversuffix) ? `-${opt.input?.etc?.semversuffix}` : ''), 107 | description:(opt.dot?.readme?.description || ''), 108 | tags:[], 109 | }, 110 | app:{ 111 | image:opt.dot.image, 112 | name:opt.dot.name, 113 | version:(opt.input?.etc?.version || opt.dot?.semver?.version), 114 | root:opt.dot.root, 115 | UID:(opt.input?.etc?.uid || 1000), 116 | GID:(opt.input?.etc?.gid || 1000), 117 | no_cache:new Date().getTime(), 118 | }, 119 | cache:{ 120 | registry:'localhost:5000/', 121 | }, 122 | tags:[], 123 | }; 124 | 125 | docker.cache.name = `${docker.image.name}:${docker.image.prefix}buildcache${docker.image.suffix}`; 126 | docker.cache.grype = `${docker.cache.registry}${docker.image.name}:${docker.image.prefix}grype${docker.image.suffix}`; 127 | docker.app.prefix = docker.image.prefix; 128 | docker.app.suffix = docker.image.suffix; 129 | 130 | // setup tags 131 | if(!opt.dot?.semver?.disable?.rolling){ 132 | docker.image.tags.push('rolling'); 133 | } 134 | if(opt.input?.etc?.dockerfile !== 'arch.dockerfile' && opt.input?.etc?.tag){ 135 | docker.image.tags.push(`${context.sha.substring(0,7)}`); 136 | docker.image.tags.push(opt.input.etc.tag); 137 | docker.image.tags.push(`${opt.input.etc.tag}-${docker.app.version}`); 138 | docker.cache.name = `${docker.image.name}:buildcache-${opt.input.etc.tag}`; 139 | }else if(docker.app.version !== 'latest'){ 140 | const semver = docker.app.version.split('.'); 141 | docker.image.tags.push(`${context.sha.substring(0,7)}`); 142 | if(Array.isArray(semver)){ 143 | if(semver.length >= 1) docker.image.tags.push(`${semver[0]}`); 144 | if(semver.length >= 2) docker.image.tags.push(`${semver[0]}.${semver[1]}`); 145 | if(semver.length >= 3) docker.image.tags.push(`${semver[0]}.${semver[1]}.${semver[2]}`); 146 | } 147 | if(opt.dot?.semver?.stable && new RegExp(opt.dot?.semver.stable, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('stable'); 148 | if(opt.dot?.semver?.latest && new RegExp(opt.dot?.semver.latest, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('latest'); 149 | }else{ 150 | docker.image.tags.push('latest'); 151 | } 152 | 153 | for(const tag of docker.image.tags){ 154 | docker.tags.push(`${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`); 155 | docker.tags.push(`ghcr.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`); 156 | docker.tags.push(`quay.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`); 157 | } 158 | 159 | // setup build arguments 160 | if(opt.input?.etc?.build?.args){ 161 | for(const arg in opt.input.etc.build.args){ 162 | docker.app[arg] = opt.input.etc.build.args[arg]; 163 | } 164 | } 165 | if(opt.dot?.build?.args){ 166 | for(const arg in opt.dot.build.args){ 167 | docker.app[arg] = opt.dot.build.args[arg]; 168 | } 169 | } 170 | const arguments = []; 171 | for(const argument in docker.app){ 172 | arguments.push(`APP_${argument.toUpperCase()}=${docker.app[argument]}`); 173 | } 174 | 175 | // export to environment 176 | core.exportVariable('DOCKER_CACHE_REGISTRY', docker.cache.registry); 177 | core.exportVariable('DOCKER_CACHE_NAME', docker.cache.name); 178 | core.exportVariable('DOCKER_CACHE_GRYPE', docker.cache.grype); 179 | 180 | core.exportVariable('DOCKER_IMAGE_NAME', docker.image.name); 181 | core.exportVariable('DOCKER_IMAGE_ARCH', docker.image.arch); 182 | core.exportVariable('DOCKER_IMAGE_TAGS', docker.tags.join(',')); 183 | core.exportVariable('DOCKER_IMAGE_DESCRIPTION', docker.image.description); 184 | core.exportVariable('DOCKER_IMAGE_ARGUMENTS', arguments.join("\r\n")); 185 | core.exportVariable('DOCKER_IMAGE_DOCKERFILE', opt.input?.etc?.dockerfile || 'arch.dockerfile'); 186 | 187 | core.exportVariable('WORKFLOW_BUILD', (opt.input?.build === undefined) ? false : opt.input.build); 188 | core.exportVariable('WORKFLOW_CREATE_RELEASE', (opt.input?.release === undefined) ? false : opt.input.release); 189 | core.exportVariable('WORKFLOW_CREATE_README', (opt.input?.readme === undefined) ? false : opt.input.readme); 190 | core.exportVariable('WORKFLOW_GRYPE_FAIL_ON_SEVERITY', (opt.dot?.grype?.fail === undefined) ? true : opt.dot.grype.fail); 191 | core.exportVariable('WORKFLOW_GRYPE_SEVERITY_CUTOFF', (opt.dot?.grype?.severity || 'high')); 192 | if(opt.dot?.readme?.comparison){ 193 | core.exportVariable('WORKFLOW_CREATE_COMPARISON', true); 194 | core.exportVariable('WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE', opt.dot.readme.comparison.image); 195 | core.exportVariable('WORKFLOW_CREATE_COMPARISON_IMAGE', `${docker.image.name}:${docker.app.version}`); 196 | } 197 | 198 | 199 | 200 | # DOCKER 201 | - name: docker / login to hub 202 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 203 | with: 204 | username: 11notes 205 | password: ${{ secrets.DOCKER_TOKEN }} 206 | 207 | - name: github / login to ghcr 208 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 209 | with: 210 | registry: ghcr.io 211 | username: 11notes 212 | password: ${{ secrets.GITHUB_TOKEN }} 213 | 214 | - name: quay / login to quay 215 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 216 | with: 217 | registry: quay.io 218 | username: 11notes+github 219 | password: ${{ secrets.QUAY_TOKEN }} 220 | 221 | - name: docker / setup qemu 222 | if: env.WORKFLOW_BUILD == 'true' 223 | uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a 224 | 225 | - name: docker / setup buildx 226 | if: env.WORKFLOW_BUILD == 'true' 227 | uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 228 | with: 229 | driver-opts: network=host 230 | 231 | - name: docker / build image locally 232 | if: env.WORKFLOW_BUILD == 'true' 233 | id: docker-build 234 | uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d 235 | with: 236 | context: . 237 | file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} 238 | push: true 239 | platforms: ${{ env.DOCKER_IMAGE_ARCH }} 240 | cache-from: type=registry,ref=${{ env.DOCKER_CACHE_NAME }} 241 | cache-to: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true 242 | build-args: | 243 | ${{ env.DOCKER_IMAGE_ARGUMENTS }} 244 | tags: | 245 | ${{ env.DOCKER_CACHE_GRYPE }} 246 | 247 | - name: grype / scan 248 | if: env.WORKFLOW_BUILD == 'true' 249 | id: grype 250 | uses: anchore/scan-action@dc6246fcaf83ae86fcc6010b9824c30d7320729e 251 | with: 252 | image: ${{ env.DOCKER_CACHE_GRYPE }} 253 | fail-build: ${{ env.WORKFLOW_GRYPE_FAIL_ON_SEVERITY }} 254 | severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} 255 | output-format: 'sarif' 256 | by-cve: true 257 | cache-db: true 258 | 259 | - name: grype / fail 260 | if: env.WORKFLOW_BUILD == 'true' && (failure() || steps.grype.outcome == 'failure') && steps.docker-build.outcome == 'success' 261 | uses: anchore/scan-action@dc6246fcaf83ae86fcc6010b9824c30d7320729e 262 | with: 263 | image: ${{ env.DOCKER_CACHE_GRYPE }} 264 | fail-build: false 265 | severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} 266 | output-format: 'table' 267 | by-cve: true 268 | cache-db: true 269 | 270 | - name: docker / build image from cache and push to registries 271 | if: env.WORKFLOW_BUILD == 'true' 272 | uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d 273 | with: 274 | context: . 275 | file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} 276 | push: true 277 | sbom: true 278 | provenance: mode=max 279 | platforms: ${{ env.DOCKER_IMAGE_ARCH }} 280 | cache-from: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }} 281 | cache-to: type=registry,ref=${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true 282 | build-args: | 283 | ${{ env.DOCKER_IMAGE_ARGUMENTS }} 284 | tags: | 285 | ${{ env.DOCKER_IMAGE_TAGS }} 286 | 287 | 288 | 289 | # RELEASE 290 | - name: github / release / markdown 291 | if: env.WORKFLOW_CREATE_RELEASE == 'true' 292 | id: git-release 293 | uses: 11notes/action-docker-release@v1 294 | # WHY IS THIS ACTION NOT SHA256 PINNED? SECURITY MUCH?!?!?! 295 | # --------------------------------------------------------------------------------- 296 | # the next step "github / release / create" creates a new release based on the code 297 | # in the repo. This code is not modified and can't be modified by this action. 298 | # It does create the markdown for the release, which could be abused, but to what 299 | # extend? Adding a link to a malicious repo? 300 | 301 | - name: github / release / create 302 | if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-release.outcome == 'success' 303 | uses: actions/create-release@4c11c9fe1dcd9636620a16455165783b20fc7ea0 304 | env: 305 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 306 | with: 307 | tag_name: ${{ github.ref }} 308 | release_name: ${{ github.ref }} 309 | body: ${{ steps.git-release.outputs.release }} 310 | draft: false 311 | prerelease: false 312 | 313 | 314 | 315 | 316 | # LICENSE 317 | - name: license / update year 318 | continue-on-error: true 319 | uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 320 | with: 321 | script: | 322 | const { existsSync, readFileSync, writeFileSync } = require('node:fs'); 323 | const { resolve } = require('node:path'); 324 | const file = 'LICENSE'; 325 | const year = new Date().getFullYear(); 326 | try{ 327 | const path = resolve(file); 328 | if(existsSync(path)){ 329 | let license = readFileSync(file).toString(); 330 | if(!new RegExp(`Copyright \\(c\\) ${year} 11notes`, 'i').test(license)){ 331 | license = license.replace(/Copyright \(c\) \d{4} /i, `Copyright (c) ${new Date().getFullYear()} `); 332 | writeFileSync(path, license); 333 | } 334 | }else{ 335 | throw new Error(`file ${file} does not exist`); 336 | } 337 | }catch(e){ 338 | core.setFailed(e); 339 | } 340 | 341 | 342 | 343 | 344 | # README 345 | - name: github / checkout HEAD 346 | continue-on-error: true 347 | run: | 348 | git checkout HEAD 349 | 350 | - name: docker / setup comparison images 351 | if: env.WORKFLOW_CREATE_COMPARISON == 'true' 352 | continue-on-error: true 353 | run: | 354 | docker image pull ${{ env.WORKFLOW_CREATE_COMPARISON_IMAGE }} 355 | docker image ls --filter "reference=${{ env.WORKFLOW_CREATE_COMPARISON_IMAGE }}" --format json | jq --raw-output '.Size' &> ./comparison.size0.log 356 | 357 | docker image pull ${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }} 358 | docker image ls --filter "reference=${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }}" --format json | jq --raw-output '.Size' &> ./comparison.size1.log 359 | 360 | docker run --entrypoint "/bin/sh" --rm ${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }} -c id &> ./comparison.id.log 361 | 362 | - name: github / create README.md 363 | id: github-readme 364 | continue-on-error: true 365 | if: env.WORKFLOW_CREATE_README == 'true' 366 | uses: 11notes/action-docker-readme@v1 367 | # WHY IS THIS ACTION NOT SHA256 PINNED? SECURITY MUCH?!?!?! 368 | # --------------------------------------------------------------------------------- 369 | # the next step "github / commit & push" only adds the README and LICENSE as well as 370 | # compose.yaml to the repository. This does not pose a security risk if this action 371 | # would be compromised. The code of the app can't be changed by this action. Since 372 | # only the files mentioned are commited to the repo. Sure, someone could make a bad 373 | # compose.yaml, but since this serves only as an example I see no harm in that. 374 | with: 375 | sarif_file: ${{ steps.grype.outputs.sarif }} 376 | build_output_metadata: ${{ steps.docker-build.outputs.metadata }} 377 | 378 | - name: docker / push README.md to docker hub 379 | continue-on-error: true 380 | if: steps.github-readme.outcome == 'success' && hashFiles('README_NONGITHUB.md') != '' 381 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 382 | env: 383 | DOCKER_USER: 11notes 384 | DOCKER_PASS: ${{ secrets.DOCKER_TOKEN }} 385 | with: 386 | destination_container_repo: ${{ env.DOCKER_IMAGE_NAME }} 387 | provider: dockerhub 388 | short_description: ${{ env.DOCKER_IMAGE_DESCRIPTION }} 389 | readme_file: 'README_NONGITHUB.md' 390 | 391 | - name: github / commit & push 392 | continue-on-error: true 393 | if: steps.github-readme.outcome == 'success' && hashFiles('README.md') != '' 394 | run: | 395 | git config user.name "github-actions[bot]" 396 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 397 | git add README.md 398 | if [ -f compose.yaml ]; then 399 | git add compose.yaml 400 | fi 401 | if [ -f compose.yml ]; then 402 | git add compose.yml 403 | fi 404 | if [ -f LICENSE ]; then 405 | git add LICENSE 406 | fi 407 | git commit -m "update README.md" 408 | git push origin HEAD:master 409 | 410 | 411 | 412 | 413 | # REPOSITORY SETTINGS 414 | - name: github / update description and set repo defaults 415 | run: | 416 | curl --request PATCH \ 417 | --url https://api.github.com/repos/${{ github.repository }} \ 418 | --header 'authorization: Bearer ${{ secrets.REPOSITORY_TOKEN }}' \ 419 | --header 'content-type: application/json' \ 420 | --data '{ 421 | "description":"${{ env.DOCKER_IMAGE_DESCRIPTION }}", 422 | "homepage":"", 423 | "has_issues":true, 424 | "has_discussions":true, 425 | "has_projects":false, 426 | "has_wiki":false 427 | }' \ 428 | --fail --------------------------------------------------------------------------------