[breaking] switch to distroless and static binary

This commit is contained in:
ElevenNotes
2025-04-14 07:23:48 +02:00
parent 5cce7c6900
commit 59ebf2bfbf
14 changed files with 589 additions and 267 deletions

View File

@@ -5,3 +5,6 @@ LICENSE
*.md *.md
img/ img/
node_modules/ node_modules/
# custom
.env

1
.gitattributes vendored
View File

@@ -1,2 +1,3 @@
# default
* text=auto * text=auto
*.sh eol=lf *.sh eol=lf

View File

@@ -10,6 +10,12 @@ on:
required: false required: false
default: 'docker' default: 'docker'
runs-on:
description: 'set runs-on for workflow (github or selfhosted)'
type: string
required: false
default: 'ubuntu-22.04'
release: release:
description: 'set WORKFLOW_GITHUB_RELEASE' description: 'set WORKFLOW_GITHUB_RELEASE'
required: false required: false
@@ -20,29 +26,14 @@ on:
required: false required: false
default: 'false' default: 'false'
image: etc:
description: 'set IMAGE' description: 'base64 encoded json string'
required: false
uid:
description: 'set IMAGE_UID'
required: false
gid:
description: 'set IMAGE_GID'
required: false
semverprefix:
description: 'prefix for semver tags'
required: false
semversuffix:
description: 'suffix for semver tags'
required: false required: false
jobs: jobs:
docker: docker:
runs-on: ubuntu-22.04 runs-on: ${{ inputs.runs-on }}
timeout-minutes: 1440
services: services:
registry: registry:
@@ -69,12 +60,17 @@ jobs:
script: | script: |
const { existsSync, readFileSync } = require('node:fs'); const { existsSync, readFileSync } = require('node:fs');
const { resolve } = require('node:path'); const { resolve } = require('node:path');
const { inspect } = require('node:util');
const { Buffer } = require('node:buffer');
const inputs = `${{ toJSON(github.event.inputs) }}`; const inputs = `${{ toJSON(github.event.inputs) }}`;
const opt = {input:{}, dot:{}}; const opt = {input:{}, dot:{}};
try{ try{
if(inputs.length > 0){ if(inputs.length > 0){
opt.input = JSON.parse(inputs); opt.input = JSON.parse(inputs);
if(opt.input?.etc){
opt.input.etc = JSON.parse(Buffer.from(opt.input.etc, 'base64').toString('ascii'));
}
} }
}catch(e){ }catch(e){
core.warning('could not parse github.event.inputs'); core.warning('could not parse github.event.inputs');
@@ -95,27 +91,30 @@ jobs:
core.setFailed(e); core.setFailed(e);
} }
core.info(inspect(opt, {showHidden:false, depth:null, colors:true}));
const docker = { const docker = {
image:{ image:{
name:(opt.input?.image || opt.dot.image), name:opt.dot.image,
arch:(opt.dot.arch || 'linux/amd64,linux/arm64'), arch:(opt.dot.arch || 'linux/amd64,linux/arm64'),
prefix:((opt.input?.semverprefix) ? `${opt.input?.semverprefix}-` : ''), prefix:((opt.input?.etc?.semverprefix) ? `${opt.input?.etc?.semverprefix}-` : ''),
suffix:((opt.input?.semversuffix) ? `-${opt.input?.semversuffix}` : ''), suffix:((opt.input?.etc?.semversuffix) ? `-${opt.input?.etc?.semversuffix}` : ''),
description:(opt.dot?.readme?.description || ''), description:(opt.dot?.readme?.description || ''),
tags:[], tags:[],
}, },
app:{ app:{
image:opt.dot.image, image:opt.dot.image,
name:opt.dot.name, name:opt.dot.name,
version:opt.dot.semver.version, version:(opt.input?.etc?.version || opt.dot.semver.version),
root:opt.dot.root, root:opt.dot.root,
UID:(opt.input?.uid || 1000), UID:(opt.input?.etc?.uid || 1000),
GID:(opt.input?.gid || 1000), GID:(opt.input?.etc?.gid || 1000),
no_cache:new Date().getTime(), no_cache:new Date().getTime(),
}, },
cache:{ cache:{
registry:'localhost:5000/', registry:'localhost:5000/',
} },
tags:[],
}; };
docker.cache.name = `${docker.image.name}:${docker.image.prefix}buildcache${docker.image.suffix}`; docker.cache.name = `${docker.image.name}:${docker.image.prefix}buildcache${docker.image.suffix}`;
@@ -124,21 +123,37 @@ jobs:
docker.app.suffix = docker.image.suffix; docker.app.suffix = docker.image.suffix;
// setup tags // setup tags
const semver = opt.dot.semver.version.split('.'); if(opt.input?.etc?.dockerfile !== 'arch.dockerfile' && opt.input?.etc?.tag){
docker.image.tags.push(`${context.sha.substring(0,7)}`); docker.image.tags.push(`${context.sha.substring(0,7)}`);
if(Array.isArray(semver)){ docker.image.tags.push(opt.input.etc.tag);
if(semver.length >= 1) docker.image.tags.push(`${semver[0]}`); docker.image.tags.push(`${opt.input.etc.tag}-${docker.app.version}`);
if(semver.length >= 2) docker.image.tags.push(`${semver[0]}.${semver[1]}`); docker.cache.name = `${docker.image.name}:buildcache-${opt.input.etc.tag}`;
if(semver.length >= 3) docker.image.tags.push(`${semver[0]}.${semver[1]}.${semver[2]}`); }else if(opt.dot?.semver?.version){
const semver = opt.dot.semver.version.split('.');
docker.image.tags.push(`${context.sha.substring(0,7)}`);
if(Array.isArray(semver)){
if(semver.length >= 1) docker.image.tags.push(`${semver[0]}`);
if(semver.length >= 2) docker.image.tags.push(`${semver[0]}.${semver[1]}`);
if(semver.length >= 3) docker.image.tags.push(`${semver[0]}.${semver[1]}.${semver[2]}`);
}
if(opt.dot.semver?.stable && new RegExp(opt.dot.semver.stable, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('stable');
if(opt.dot.semver?.latest && new RegExp(opt.dot.semver.latest, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('latest');
}else if(opt.input?.etc?.version && opt.input.etc.version === 'latest'){
docker.image.tags.push('latest');
} }
if(opt.dot.semver?.stable && new RegExp(opt.dot.semver.stable, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('stable');
if(opt.dot.semver?.latest && new RegExp(opt.dot.semver.latest, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('latest');
for(let i=0; i<docker.image.tags.length; i++){ for(const tag of docker.image.tags){
docker.image.tags[i] = `${docker.image.name}:${docker.image.prefix}${docker.image.tags[i]}${docker.image.suffix}`; docker.tags.push(`${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`);
docker.tags.push(`ghcr.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`);
docker.tags.push(`quay.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`);
} }
// setup build arguments // setup build arguments
if(opt.input?.etc?.build?.args){
for(const arg in opt.input.etc.build.args){
docker.app[arg] = opt.input.etc.build.args[arg];
}
}
const arguments = []; const arguments = [];
for(const argument in docker.app){ for(const argument in docker.app){
arguments.push(`APP_${argument.toUpperCase()}=${docker.app[argument]}`); arguments.push(`APP_${argument.toUpperCase()}=${docker.app[argument]}`);
@@ -151,24 +166,39 @@ jobs:
core.exportVariable('DOCKER_IMAGE_NAME', docker.image.name); core.exportVariable('DOCKER_IMAGE_NAME', docker.image.name);
core.exportVariable('DOCKER_IMAGE_ARCH', docker.image.arch); core.exportVariable('DOCKER_IMAGE_ARCH', docker.image.arch);
core.exportVariable('DOCKER_IMAGE_TAGS', docker.image.tags.join(',')); core.exportVariable('DOCKER_IMAGE_TAGS', docker.tags.join(','));
core.exportVariable('DOCKER_IMAGE_DESCRIPTION', docker.image.description); core.exportVariable('DOCKER_IMAGE_DESCRIPTION', docker.image.description);
core.exportVariable('DOCKER_IMAGE_ARGUMENTS', arguments.join("\r\n")); core.exportVariable('DOCKER_IMAGE_ARGUMENTS', arguments.join("\r\n"));
core.exportVariable('DOCKER_IMAGE_DOCKERFILE', opt.input?.etc?.dockerfile || 'arch.dockerfile');
core.exportVariable('WORKFLOW_CREATE_RELEASE', (opt.input?.release || true)); core.exportVariable('WORKFLOW_CREATE_RELEASE', (opt.input?.release === undefined) ? false : opt.input.release);
core.exportVariable('WORKFLOW_CREATE_README', (opt.input?.readme || true)); core.exportVariable('WORKFLOW_CREATE_README', (opt.input?.readme === undefined) ? false : opt.input.readme);
core.exportVariable('WORKFLOW_GRYPE_FAIL_ON_SEVERITY', (opt.json?.grpye?.fail || true)); core.exportVariable('WORKFLOW_GRYPE_FAIL_ON_SEVERITY', (opt.dot?.grype?.fail === undefined) ? true : opt.dot.grype.fail);
core.exportVariable('WORKFLOW_GRYPE_SEVERITY_CUTOFF', (opt.json?.grpye?.severity || 'high')); core.exportVariable('WORKFLOW_GRYPE_SEVERITY_CUTOFF', (opt.dot?.grype?.severity || 'high'));
# DOCKER # DOCKER
- name: docker / login to hub - name: docker / login to hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
with: with:
username: 11notes username: 11notes
password: ${{ secrets.DOCKER_TOKEN }} password: ${{ secrets.DOCKER_TOKEN }}
- name: github / login to ghcr
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
with:
registry: ghcr.io
username: 11notes
password: ${{ secrets.GITHUB_TOKEN }}
- name: quay / login to quay
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
with:
registry: quay.io
username: 11notes+github
password: ${{ secrets.QUAY_TOKEN }}
- name: docker / setup qemu - name: docker / setup qemu
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a
@@ -182,7 +212,7 @@ jobs:
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d
with: with:
context: . context: .
file: arch.dockerfile file: ${{ env.DOCKER_IMAGE_DOCKERFILE }}
push: true push: true
platforms: ${{ env.DOCKER_IMAGE_ARCH }} platforms: ${{ env.DOCKER_IMAGE_ARCH }}
cache-from: type=registry,ref=${{ env.DOCKER_CACHE_NAME }} cache-from: type=registry,ref=${{ env.DOCKER_CACHE_NAME }}
@@ -194,7 +224,7 @@ jobs:
- name: grype / scan - name: grype / scan
id: grype id: grype
uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 uses: anchore/scan-action@dc6246fcaf83ae86fcc6010b9824c30d7320729e
with: with:
image: ${{ env.DOCKER_CACHE_GRYPE }} image: ${{ env.DOCKER_CACHE_GRYPE }}
fail-build: ${{ env.WORKFLOW_GRYPE_FAIL_ON_SEVERITY }} fail-build: ${{ env.WORKFLOW_GRYPE_FAIL_ON_SEVERITY }}
@@ -205,7 +235,7 @@ jobs:
- name: grype / fail - name: grype / fail
if: failure() || steps.grype.outcome == 'failure' if: failure() || steps.grype.outcome == 'failure'
uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 uses: anchore/scan-action@dc6246fcaf83ae86fcc6010b9824c30d7320729e
with: with:
image: ${{ env.DOCKER_CACHE_GRYPE }} image: ${{ env.DOCKER_CACHE_GRYPE }}
fail-build: false fail-build: false
@@ -218,7 +248,7 @@ jobs:
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d
with: with:
context: . context: .
file: arch.dockerfile file: ${{ env.DOCKER_IMAGE_DOCKERFILE }}
push: true push: true
sbom: true sbom: true
provenance: mode=max provenance: mode=max
@@ -250,6 +280,12 @@ jobs:
if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-log.outcome == 'success' if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-log.outcome == 'success'
id: git-release id: git-release
uses: 11notes/action-docker-release@v1 uses: 11notes/action-docker-release@v1
# WHY IS THIS ACTION NOT SHA256 PINNED? SECURITY MUCH?!?!?!
# ---------------------------------------------------------------------------------
# the next step "github / release / create" creates a new release based on the code
# in the repo. This code is not modified and can't be modified by this action.
# It does create the markdown for the release, which could be abused, but to what
# extend? Adding a link to a malicious repo?
with: with:
git_log: ${{ steps.git-log.outputs.commits }} git_log: ${{ steps.git-log.outputs.commits }}
@@ -267,6 +303,35 @@ jobs:
# LICENSE
- name: license / update year
continue-on-error: true
uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298
with:
script: |
const { existsSync, readFileSync, writeFileSync } = require('node:fs');
const { resolve } = require('node:path');
const file = 'LICENSE';
const year = new Date().getFullYear();
try{
const path = resolve(file);
if(existsSync(path)){
let license = readFileSync(file).toString();
if(!new RegExp(`Copyright \\(c\\) ${year} 11notes`, 'i').test(license)){
license = license.replace(/Copyright \(c\) \d{4} /i, `Copyright (c) ${new Date().getFullYear()} `);
writeFileSync(path, license);
}
}else{
throw new Error(`file ${file} does not exist`);
}
}catch(e){
core.setFailed(e);
}
# README # README
- name: github / checkout master - name: github / checkout master
continue-on-error: true continue-on-error: true
@@ -278,23 +343,20 @@ jobs:
continue-on-error: true continue-on-error: true
if: env.WORKFLOW_CREATE_README == 'true' && steps.docker-build.outcome == 'success' if: env.WORKFLOW_CREATE_README == 'true' && steps.docker-build.outcome == 'success'
uses: 11notes/action-docker-readme@v1 uses: 11notes/action-docker-readme@v1
# WHY IS THIS ACTION NOT SHA256 PINNED? SECURITY MUCH?!?!?!
# ---------------------------------------------------------------------------------
# the next step "github / commit & push" only adds the README and LICENSE as well as
# compose.yaml to the repository. This does not pose a security risk if this action
# would be compromised. The code of the app can't be changed by this action. Since
# only the files mentioned are commited to the repo. Sure, someone could make a bad
# compose.yaml, but since this serves only as an example I see no harm in that.
with: with:
sarif_file: ${{ steps.grype.outputs.sarif }} sarif_file: ${{ steps.grype.outputs.sarif }}
build_output_metadata: ${{ steps.docker-build.outputs.metadata }} build_output_metadata: ${{ steps.docker-build.outputs.metadata }}
- name: github / commit & push
continue-on-error: true
if: steps.github-readme.outcome == 'success' && hashFiles('README.md') != ''
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add README.md
git commit -m "auto update README.md"
git push
- name: docker / push README.md to docker hub - name: docker / push README.md to docker hub
continue-on-error: true continue-on-error: true
if: steps.github-readme.outcome == 'success' && hashFiles('README.md') != '' if: steps.github-readme.outcome == 'success' && hashFiles('README_NONGITHUB.md') != ''
uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8
env: env:
DOCKER_USER: 11notes DOCKER_USER: 11notes
@@ -303,7 +365,35 @@ jobs:
destination_container_repo: ${{ env.DOCKER_IMAGE_NAME }} destination_container_repo: ${{ env.DOCKER_IMAGE_NAME }}
provider: dockerhub provider: dockerhub
short_description: ${{ env.DOCKER_IMAGE_DESCRIPTION }} short_description: ${{ env.DOCKER_IMAGE_DESCRIPTION }}
readme_file: 'README.md' readme_file: 'README_NONGITHUB.md'
- name: quay / push README.md to quay
continue-on-error: true
if: steps.github-readme.outcome == 'success' && hashFiles('README_NONGITHUB.md') != ''
uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8
env:
DOCKER_APIKEY: ${{ secrets.QUAY_TOKEN }}
with:
destination_container_repo: quay.io/${{ env.DOCKER_IMAGE_NAME }}
provider: quay
readme_file: 'README_NONGITHUB.md'
- name: github / commit & push
continue-on-error: true
if: steps.github-readme.outcome == 'success' && hashFiles('README.md') != ''
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add README.md
if [ -f compose.yaml ]; then
git add compose.yaml
fi
if [ -f LICENSE ]; then
git add LICENSE
fi
git commit -m "auto update README.md"
git push

36
.github/workflows/full.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: tags
on:
push:
tags:
- 'full'
jobs:
node:
runs-on: ubuntu-latest
steps:
- name: init / base64 nested json
uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298
with:
script: |
const { Buffer } = require('node:buffer');
(async()=>{
try{
const etc = {
build:{
args:{
NGINX_CONFIGURATION:'full',
}
},
semverprefix:'full',
};
core.exportVariable('WORKFLOW_BASE64JSON', Buffer.from(JSON.stringify(etc)).toString('base64'));
}catch(e){
core.setFailed(`workflow failed: ${e}`);
}
})();
- name: build docker image
uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7
with:
workflow: docker.yml
token: "${{ secrets.REPOSITORY_TOKEN }}"
inputs: '{ "release":"false", "readme":"true", "etc":"${{ env.WORKFLOW_BASE64JSON }}" }'

View File

@@ -4,22 +4,32 @@ on:
tags: tags:
- 'v*' - 'v*'
jobs: jobs:
docker: node:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: init / base64 nested json
uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298
with:
script: |
const { Buffer } = require('node:buffer');
(async()=>{
try{
const etc = {
build:{
args:{
NGINX_CONFIGURATION:'light',
}
}
};
core.exportVariable('WORKFLOW_BASE64JSON', Buffer.from(JSON.stringify(etc)).toString('base64'));
}catch(e){
core.setFailed(`workflow failed: ${e}`);
}
})();
- name: build docker image - name: build docker image
uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7
with: with:
workflow: docker.yml workflow: docker.yml
token: "${{ secrets.REPOSITORY_TOKEN }}" token: "${{ secrets.REPOSITORY_TOKEN }}"
inputs: '{ "release":"true", "readme":"true" }' inputs: '{ "release":"true", "readme":"true", "etc":"${{ env.WORKFLOW_BASE64JSON }}" }'
docker-unraid:
runs-on: ubuntu-latest
steps:
- name: build docker image for unraid community
uses: the-actions-org/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7
with:
workflow: docker.yml
token: "${{ secrets.REPOSITORY_TOKEN }}"
inputs: '{ "release":"false", "readme":"false", "uid":"99", "gid":"100", "semversuffix":"unraid", "run-name":"docker-unraid" }'

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# default # default
maintain/ maintain/
node_modules/ node_modules/
# custom
.env

11
.json
View File

@@ -10,12 +10,15 @@
}, },
"readme":{ "readme":{
"description":"Nginx with additional plugins and custom compiled", "description":"Nginx, slim and distroless to be used behind a reverse proxy or as full version",
"parent":{
"image":"11notes/alpine:stable"
},
"built":{ "built":{
"nginx":"https://nginx.org" "nginx":"https://nginx.org"
},
"distroless":{
"layers":[
"11notes/distroless",
"11notes/distroless:curl"
]
} }
} }
} }

View File

@@ -1,100 +1,297 @@
ARG APP_UID=1000
ARG APP_GID=1000
# :: Util # :: Util
FROM 11notes/util AS util FROM 11notes/util AS util
# :: Build / nginx # :: Build / nginx
FROM alpine/git AS build FROM alpine AS build
ARG APP_VERSION ARG TARGETARCH
ARG TARGETPLATFORM
ARG TARGETVARIANT
ARG APP_ROOT ARG APP_ROOT
ENV MODULE_HEADERS_MORE_NGINX_VERSION=0.37 ARG APP_VERSION
ARG APP_NGINX_CONFIGURATION
ENV BUILD_ROOT=/nginx-${APP_VERSION}
ENV BUILD_BIN=${BUILD_ROOT}/objs/nginx
ENV NGINX_PREFIX=/etc/nginx
ENV BUILD_DEPENDENCY_OPENSSL_VERSION=3.5.0
ENV BUILD_DEPENDENCY_OPENSSL_ROOT=/openssl-${BUILD_DEPENDENCY_OPENSSL_VERSION}
ENV BUILD_DEPENDENCY_ZLIB_VERSION=1.3.1
ENV BUILD_DEPENDENCY_ZLIB_ROOT=/zlib-${BUILD_DEPENDENCY_ZLIB_VERSION}
ENV BUILD_DEPENDENCY_PCRE2_VERSION=10.45
ENV BUILD_DEPENDENCY_PCRE2_ROOT=/pcre2-${BUILD_DEPENDENCY_PCRE2_VERSION}
ENV BUILD_DEPENDENCY_HEADERS_MORE_VERSION=0.38
ENV BUILD_DEPENDENCY_HEADERS_MORE_ROOT=/headers-more-nginx-module-${BUILD_DEPENDENCY_HEADERS_MORE_VERSION}
ENV BUILD_DEPENDENCY_BROTLI_ROOT=/ngx_brotli
ENV BUILD_DEPENDENCY_NJS_VERSION=0.8.10
ENV BUILD_DEPENDENCY_NJS_ROOT=/njs-${BUILD_DEPENDENCY_NJS_VERSION}
ENV BUILD_DEPENDENCY_QUICKJS_VERSION=
ENV BUILD_DEPENDENCY_QUICKJS_ROOT=/quickjs${BUILD_DEPENDENCY_QUICKJS_VERSION}
USER root
COPY --from=util /usr/local/bin/ /usr/local/bin
RUN set -ex; \ RUN set -ex; \
CONFIG="\ apk --update --no-cache add \
--with-cc-opt=-O2 \ cmake \
--prefix=/etc/nginx \ autoconf \
--sbin-path=/usr/sbin/nginx \ automake \
--modules-path=/usr/lib/nginx/modules \ git \
--conf-path=/etc/nginx/nginx.conf \ build-base \
--error-log-path=/var/log/nginx/error.log \ curl \
--http-log-path=/var/log/nginx/access.log \ tar \
--pid-path=${APP_ROOT}/run/nginx.pid \ gcc \
--lock-path=${APP_ROOT}/run/nginx.lock \ g++ \
--http-client-body-temp-path=${APP_ROOT}/cache/client_temp \ libc-dev \
--http-proxy-temp-path=${APP_ROOT}/cache/proxy_temp \ make \
--http-fastcgi-temp-path=${APP_ROOT}/cache/fastcgi_temp \ openssl-dev \
--http-uwsgi-temp-path=${APP_ROOT}/cache/uwsgi_temp \ pcre2-dev \
--http-scgi-temp-path=${APP_ROOT}/cache/scgi_temp \ zlib-dev \
--user=docker \ linux-headers \
--group=docker \ libxslt-dev \
--with-http_ssl_module \ libxslt-static \
--with-http_realip_module \ gd-dev \
--with-http_addition_module \ geoip-dev \
--with-http_sub_module \ perl-dev \
--with-http_dav_module \ libedit-dev \
--with-http_flv_module \ libxml2-dev \
--with-http_mp4_module \ libtool \
--with-http_gunzip_module \ quickjs-dev \
--with-http_gzip_static_module \ quickjs-static \
--with-http_random_index_module \ bash \
--with-http_secure_link_module \ libxml2-static \
--with-http_stub_status_module \ alpine-sdk \
--with-http_auth_request_module \ findutils \
--with-http_xslt_module=dynamic \ brotli-dev \
--with-http_image_filter_module=dynamic \ libgd \
--with-http_geoip_module=dynamic \ tar \
--with-threads \ xz \
--with-stream \ upx;
--with-stream_ssl_module \
--with-stream_ssl_preread_module \ RUN set -ex; \
--with-stream_realip_module \ cd /; \
--with-stream_geoip_module=dynamic \ curl -SL https://nginx.org/download/nginx-${APP_VERSION}.tar.gz | tar -zxC /; \
--with-http_slice_module \ curl -SL https://zlib.net/fossils/zlib-${BUILD_DEPENDENCY_ZLIB_VERSION}.tar.gz | tar -zxC /; \
--with-compat \ curl -SL https://github.com/PCRE2Project/pcre2/releases/download/pcre2-${BUILD_DEPENDENCY_PCRE2_VERSION}/pcre2-${BUILD_DEPENDENCY_PCRE2_VERSION}.tar.gz | tar -zxC /; \
--with-file-aio \ curl -SL https://github.com/openresty/headers-more-nginx-module/archive/v${BUILD_DEPENDENCY_HEADERS_MORE_VERSION}.tar.gz | tar -zxC /;
--with-http_v2_module \
--add-module=/usr/lib/nginx/modules/headers-more-nginx-module-${MODULE_HEADERS_MORE_NGINX_VERSION} \ RUN set -ex; \
"; \ #build OpenSSL
apk add --no-cache --update \ case "${APP_NGINX_CONFIGURATION}" in \
curl \ "full") \
tar \ cd /; \
gcc \ curl -SL https://github.com/openssl/openssl/releases/download/openssl-${BUILD_DEPENDENCY_OPENSSL_VERSION}/openssl-${BUILD_DEPENDENCY_OPENSSL_VERSION}.tar.gz | tar -zxC /; \
libc-dev \ ;; \
make \ esac;
openssl-dev \
pcre2-dev \ RUN set -ex; \
zlib-dev \ # build brotli
linux-headers \ cd /; \
libxslt-dev \ git clone --recurse-submodules -j8 https://github.com/google/ngx_brotli; \
gd-dev \ mkdir -p ${BUILD_DEPENDENCY_BROTLI_ROOT}/deps/brotli/out; \
geoip-dev \ cd ${BUILD_DEPENDENCY_BROTLI_ROOT}/deps/brotli/out; \
perl-dev \ cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS="-Ofast -m64 -march=native -mtune=native -flto -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_CXX_FLAGS="-Ofast -m64 -march=native -mtune=native -flto -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_INSTALL_PREFIX=./installed ..; \
libedit-dev \ cmake --build . --config Release --target brotlienc;
bash \
alpine-sdk \ RUN set -ex; \
findutils; \ #build QuickJS
apk upgrade; \ case "${APP_NGINX_CONFIGURATION}" in \
mkdir -p /usr/lib/nginx/modules; \ "full") \
mkdir -p /usr/src; \ cd /; \
curl -SL https://github.com/openresty/headers-more-nginx-module/archive/v${MODULE_HEADERS_MORE_NGINX_VERSION}.tar.gz | tar -zxC /usr/lib/nginx/modules; \ git clone https://github.com/bellard/quickjs; \
curl -SL https://nginx.org/download/nginx-${APP_VERSION}.tar.gz | tar -zxC /usr/src; \ curl -SL https://github.com/nginx/njs/archive/refs/tags/${BUILD_DEPENDENCY_NJS_VERSION}.tar.gz | tar -zxC /; \
cd /usr/src/nginx-${APP_VERSION}; \ cd ${BUILD_DEPENDENCY_QUICKJS_ROOT}; \
./configure $CONFIG --with-debug; \ CFLAGS='-fPIC -static -static-libgcc' make libquickjs.a; \
make -j $(nproc); \ ;; \
mv objs/nginx objs/nginx-debug; \ esac;
mv objs/ngx_http_xslt_filter_module.so objs/ngx_http_xslt_filter_module-debug.so; \
mv objs/ngx_http_image_filter_module.so objs/ngx_http_image_filter_module-debug.so; \ RUN set -ex; \
mv objs/ngx_http_geoip_module.so objs/ngx_http_geoip_module-debug.so; \ #build XLST
mv objs/ngx_stream_geoip_module.so objs/ngx_stream_geoip_module-debug.so; \ case "${APP_NGINX_CONFIGURATION}" in \
./configure $CONFIG; \ "full") \
make -j $(nproc); \ cd /; \
make install; \ curl -SL https://download.gnome.org/sources/libxml2/2.14/libxml2-2.14.1.tar.xz | tar -xJC /; \
install -m755 objs/ngx_http_xslt_filter_module-debug.so /usr/lib/nginx/modules/ngx_http_xslt_filter_module-debug.so; \ curl -SL https://download.gnome.org/sources/libxslt/1.1/libxslt-1.1.43.tar.xz | tar -xJC /; \
install -m755 objs/ngx_http_image_filter_module-debug.so /usr/lib/nginx/modules/ngx_http_image_filter_module-debug.so; \ cd /libxml2-2.14.1; \
install -m755 objs/ngx_http_geoip_module-debug.so /usr/lib/nginx/modules/ngx_http_geoip_module-debug.so; \ ./configure \
install -m755 objs/ngx_stream_geoip_module-debug.so /usr/lib/nginx/modules/ngx_stream_geoip_module-debug.so; \ --prefix="/usr" \
strip /usr/sbin/nginx*; \ --disable-shared \
strip /usr/lib/nginx/modules/*.so; --enable-static \
--without-python; \
make -s -j $(nproc); \
make install; \
cd /libxslt-1.1.43; \
./configure \
--prefix="/usr" \
--disable-shared \
--enable-static \
--without-python; \
make -s -j $(nproc); \
make install; \
;; \
esac;
RUN set -ex; \
case "${APP_NGINX_CONFIGURATION}" in \
"light") \
cd ${BUILD_ROOT}; \
./configure \
--with-zlib=${BUILD_DEPENDENCY_ZLIB_ROOT} \
--with-pcre=${BUILD_DEPENDENCY_PCRE2_ROOT} \
--add-module=${BUILD_DEPENDENCY_HEADERS_MORE_ROOT} \
--add-module=${BUILD_DEPENDENCY_BROTLI_ROOT} \
--prefix=${NGINX_PREFIX} \
--sbin-path=${BUILD_BIN} \
--modules-path=${APP_ROOT}/lib/modules \
--conf-path=${NGINX_PREFIX}/nginx.conf \
--error-log-path=${APP_ROOT}/log/error.log \
--http-log-path=${APP_ROOT}/log/access.log \
--pid-path=${APP_ROOT}/run/nginx.pid \
--lock-path=${APP_ROOT}/run/nginx.lock \
--http-client-body-temp-path=${APP_ROOT}/cache/client_temp \
--http-proxy-temp-path=${APP_ROOT}/cache/proxy_temp \
--http-fastcgi-temp-path=${APP_ROOT}/cache/fastcgi_temp \
--http-uwsgi-temp-path=${APP_ROOT}/cache/uwsgi_temp \
--http-scgi-temp-path=${APP_ROOT}/cache/scgi_temp \
--user=docker \
--group=docker \
--with-file-aio \
--with-poll_module \
--with-select_module \
--with-http_addition_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_mp4_module \
--with-http_realip_module \
--with-http_stub_status_module \
--with-http_sub_module \
--with-http_v2_module \
--without-http_autoindex_module \
--without-http_browser_module \
--without-http_charset_module \
--without-http_empty_gif_module \
--without-http_geo_module \
--without-http_memcached_module \
--without-http_map_module \
--without-http_ssi_module \
--without-http_split_clients_module \
--without-http_fastcgi_module \
--without-http_uwsgi_module \
--without-http_userid_module \
--without-http_scgi_module \
--without-mail_pop3_module \
--without-mail_imap_module \
--without-mail_smtp_module \
--with-cc-opt="-O2 -static -static-libgcc" \
--with-ld-opt="-s -static"; \
;; \
"full") \
cd ${BUILD_ROOT}; \
./configure \
--with-zlib=${BUILD_DEPENDENCY_ZLIB_ROOT} \
--with-pcre=${BUILD_DEPENDENCY_PCRE2_ROOT} \
--add-module=${BUILD_DEPENDENCY_HEADERS_MORE_ROOT} \
--add-module=${BUILD_DEPENDENCY_BROTLI_ROOT} \
--add-module=${BUILD_DEPENDENCY_NJS_ROOT}/nginx \
--with-openssl=${BUILD_DEPENDENCY_OPENSSL_ROOT} \
--prefix=${NGINX_PREFIX} \
--sbin-path=${BUILD_BIN} \
--modules-path=${APP_ROOT}/lib/modules \
--conf-path=${NGINX_PREFIX}/nginx.conf \
--error-log-path=${APP_ROOT}/log/error.log \
--http-log-path=${APP_ROOT}/log/access.log \
--pid-path=${APP_ROOT}/run/nginx.pid \
--lock-path=${APP_ROOT}/run/nginx.lock \
--http-client-body-temp-path=${APP_ROOT}/cache/client_temp \
--http-proxy-temp-path=${APP_ROOT}/cache/proxy_temp \
--http-fastcgi-temp-path=${APP_ROOT}/cache/fastcgi_temp \
--http-uwsgi-temp-path=${APP_ROOT}/cache/uwsgi_temp \
--http-scgi-temp-path=${APP_ROOT}/cache/scgi_temp \
--user=docker \
--group=docker \
--with-http_ssl_module \
--with-http_realip_module \
--with-http_addition_module \
--with-http_sub_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_mp4_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_random_index_module \
--with-http_secure_link_module \
--with-http_stub_status_module \
--with-http_auth_request_module \
--with-http_geoip_module \
--with-threads \
--with-stream \
--with-stream_ssl_module \
--with-stream_ssl_preread_module \
--with-stream_realip_module \
--with-stream_geoip_module \
--with-http_slice_module \
--with-http_xslt_module \
--with-mail \
--with-mail_ssl_module \
--with-compat \
--with-file-aio \
--with-http_v2_module \
--with-cc-opt="-O2 -static -static-libgcc -I ${BUILD_DEPENDENCY_QUICKJS_ROOT}" \
--with-ld-opt="-s -static -L ${BUILD_DEPENDENCY_QUICKJS_ROOT}"; \
;; \
esac;
RUN set -ex; \
cd ${BUILD_ROOT}; \
make -s -j $(nproc); \
eleven checkStatic ${BUILD_BIN};
RUN set -ex; \
mkdir -p /distroless/usr/local/bin; \
mkdir -p /distroless${NGINX_PREFIX}; \
cp -R ${BUILD_ROOT}/conf/* /distroless${NGINX_PREFIX}; \
rm /distroless${NGINX_PREFIX}/nginx.conf; \
eleven strip ${BUILD_BIN}; \
cp ${BUILD_BIN} /distroless/usr/local/bin;
COPY ./rootfs/etc /distroless/etc
# :: Distroless / nginx
FROM scratch AS distroless-nginx
COPY --from=build /distroless/ /
# :: Build / file system
FROM alpine AS fs
ARG APP_ROOT
USER root
RUN set -ex; \
mkdir -p ${APP_ROOT}/etc; \
mkdir -p ${APP_ROOT}/var; \
mkdir -p ${APP_ROOT}/run; \
mkdir -p ${APP_ROOT}/lib/modules; \
mkdir -p ${APP_ROOT}/cache; \
mkdir -p ${APP_ROOT}/log; \
ln -sf /dev/stdout ${APP_ROOT}/log/access.log; \
ln -sf /dev/stderr ${APP_ROOT}/log/error.log;
COPY ./rootfs/nginx ${APP_ROOT}
# :: Distroless / file system
FROM scratch AS distroless-fs
ARG APP_ROOT
COPY --from=fs ${APP_ROOT} /${APP_ROOT}
# :: Header # :: Header
FROM 11notes/alpine:stable FROM 11notes/distroless AS distroless
FROM 11notes/distroless:curl AS distroless-curl
FROM scratch
# :: arguments # :: arguments
ARG TARGETARCH ARG TARGETARCH
@@ -111,50 +308,18 @@
ENV APP_VERSION=${APP_VERSION} ENV APP_VERSION=${APP_VERSION}
ENV APP_ROOT=${APP_ROOT} ENV APP_ROOT=${APP_ROOT}
ENV NGINX_HEALTHCHECK_URL="https://localhost:8443/ping"
# :: multi-stage # :: multi-stage
COPY --from=util /usr/local/bin/ /usr/local/bin COPY --from=distroless --chown=${APP_UID}:${APP_GID} / /
COPY --from=build /usr/sbin/nginx /usr/sbin COPY --from=distroless-fs --chown=${APP_UID}:${APP_GID} / /
COPY --from=build /etc/nginx/ /etc/nginx COPY --from=distroless-curl --chown=${APP_UID}:${APP_GID} / /
COPY --from=build /usr/lib/nginx/modules/ /etc/nginx/modules COPY --from=distroless-nginx --chown=${APP_UID}:${APP_GID} / /
# :: Run
USER root
RUN eleven printenv;
# :: install application
RUN set -ex; \
apk --no-cache --update add \
inotify-tools \
openssl \
pcre2-dev;
RUN set -ex; \
eleven mkdir ${APP_ROOT}/{etc,var,ssl,cache,run}; \
mkdir -p /var/log/nginx; \
touch /var/log/nginx/access.log; \
touch /var/log/nginx/error.log; \
ln -sf /dev/stdout /var/log/nginx/access.log; \
ln -sf /dev/stderr /var/log/nginx/error.log;
# :: copy filesystem changes and set correct permissions
COPY ./rootfs /
RUN set -ex; \
chmod +x -R /usr/local/bin; \
chown -R 1000:1000 \
${APP_ROOT} \
/var/log/nginx;
# :: support unraid
RUN set -ex; \
eleven unraid;
# :: Volumes # :: Volumes
VOLUME ["${APP_ROOT}/etc", "${APP_ROOT}/var"] VOLUME ["${APP_ROOT}/etc", "${APP_ROOT}/var"]
# :: Monitor # :: Monitor
HEALTHCHECK --interval=5s --timeout=2s CMD curl -X GET -kILs --fail ${NGINX_HEALTHCHECK_URL} || exit 1 HEALTHCHECK --interval=5s --timeout=2s CMD ["/usr/local/bin/curl", "-kILs", "--fail", "http://localhost:3000/ping"]
# :: Start # :: Start
USER docker USER ${APP_UID}:${APP_GID}
ENTRYPOINT ["/usr/local/bin/nginx"]

View File

@@ -1,17 +1,25 @@
name: "nginx"
services: services:
nginx: nginx:
image: "11notes/nginx:1.26.2" image: "11notes/nginx:1.26.3"
container_name: "nginx" read_only: true
environment: environment:
TZ: "Europe/Zurich" TZ: "Europe/Zurich"
ports: ports:
- "8443:8443/tcp" - "3000:3000/tcp"
networks:
frontend:
volumes: volumes:
- "etc:/nginx/etc" - "etc:/nginx/etc"
- "var:/nginx/var" - "var:/nginx/var"
- "ssl:/nginx/ssl" tmpfs:
- "/nginx/cache:uid=1000,gid=1000"
- "/nginx/run:uid=1000,gid=1000"
restart: "always" restart: "always"
volumes: volumes:
etc: etc:
var: var:
ssl:
networks:
frontend:

View File

@@ -1,7 +1,26 @@
${{ content_synopsis }} What can I do with this? This image will serve as a base for nginx related images that need a high-performance webserver. It can also be used stand alone as a webserver or reverse proxy. It will automatically reload on config changes if configured. ${{ content_synopsis }} This image will serve as a base for nginx related images that need a high-performance webserver. The default tag of this image is stripped for most functions that can be used by a reverse proxy in front of nginx, it adds however important webserver functions like brotli compression. The default tag is not meant to run as a reverse proxy, use the full image for that. The default tag does not support HTTPS for instance!
${{ content_uvp }} Good question! All the other images on the market that do exactly the same dont do or offer these options:
${{ github:> [!IMPORTANT] }}
${{ github:> }}* This image runs as 1000:1000 by default, most other images run everything as root
${{ github:> }}* This image has no shell since it is 100% distroless, most other images run on a distro like Debian or Alpine with full shell access (security)
${{ github:> }}* This image does not ship with any critical or high rated CVE and is automatically maintained via CI/CD, most other images mostly have no CVE scanning or code quality tools in place
${{ github:> }}* This image is created via a secure, pinned CI/CD process and immune to upstream attacks, most other images have upstream dependencies that can be exploited
${{ github:> }}* This image contains a proper health check that verifies the app is actually working, most other images have either no health check or only check if a port is open or ping works
${{ github:> }}* This image works as read-only, most other images need to write files to the image filesystem
If you value security, simplicity and the ability to interact with the maintainer and developer of an image. Using my images is a great start in that direction.
${{ title_config }}
```yaml
${{ include: ./rootfs/etc/nginx/nginx.conf }}
```
The default configuration contains no special settings. It enables brotli compression, sets the workers to the same amount as n-CPUs available, has two default logging formats, disables most stuff not needed and enables best performance settings. Please mount your own config if you need to change how nginx is setup.
${{ title_volumes }} ${{ title_volumes }}
* **${{ json_root }}/etc** - Directory of vHost config, must end in *.conf (set in /etc/nginx/nginx.conf) * **${{ json_root }}/etc** - Directory of vHost config, must end in *.conf
* **${{ json_root }}/var** - Directory of webroot for vHost * **${{ json_root }}/var** - Directory of webroot for vHost
${{ content_compose }} ${{ content_compose }}
@@ -9,8 +28,6 @@ ${{ content_compose }}
${{ content_defaults }} ${{ content_defaults }}
${{ content_environment }} ${{ content_environment }}
| `NGINX_DYNAMIC_RELOAD` | Enable reload of nginx on configuration changes in /nginx/etc (only on successful configuration test!) | |
| `NGINX_HEALTHCHECK_URL` | URL to check if nginx is ready to accept connections | https://localhost:8443/ping |
${{ content_source }} ${{ content_source }}

View File

@@ -1,7 +1,8 @@
worker_processes auto; worker_processes auto;
worker_cpu_affinity auto; worker_cpu_affinity auto;
worker_rlimit_nofile 204800; worker_rlimit_nofile 204800;
error_log /var/log/nginx/error.log warn; error_log /nginx/log/error.log warn;
daemon off;
events { events {
worker_connections 1024; worker_connections 1024;
@@ -25,6 +26,36 @@ http {
tcp_nodelay on; tcp_nodelay on;
gzip on; gzip on;
brotli on;
brotli_comp_level 4;
brotli_static on;
brotli_types
text/plain
text/css
text/xml
text/javascript
text/x-component
application/xml
application/xml+rss
application/javascript
application/json
application/atom+xml
application/vnd.ms-fontobject
application/x-font-ttf
application/x-font-opentype
application/x-font-truetype
application/x-web-app-manifest+json
application/xhtml+xml
application/octet-stream
font/opentype
font/truetype
font/eot
font/otf
image/svg+xml
image/x-icon
image/vnd.microsoft.icon
image/bmp;
client_max_body_size 8M; client_max_body_size 8M;
keepalive_timeout 90; keepalive_timeout 90;
keepalive_requests 102400; keepalive_requests 102400;
@@ -37,5 +68,7 @@ http {
open_file_cache_min_uses 2; open_file_cache_min_uses 2;
open_file_cache_errors off; open_file_cache_errors off;
root /nginx/var;
include /nginx/etc/*.conf; include /nginx/etc/*.conf;
} }

View File

@@ -1,9 +1,10 @@
server { server {
listen 8443 default_server ssl; listen 3000 default_server;
server_name _; server_name _;
ssl_certificate /nginx/ssl/default.crt; location / {
ssl_certificate_key /nginx/ssl/default.key; return 404;
}
location /ping { location /ping {
return 200; return 200;

View File

@@ -1,22 +0,0 @@
#!/bin/ash
if { [ ! -f "${APP_ROOT}/ssl/default.crt" ] && [ -f "${APP_ROOT}/etc/default.conf" ] && cat ${APP_ROOT}/etc/default.conf | grep -q "default.crt"; }; then
eleven log debug "creating default certificate"
openssl req -x509 -newkey rsa:4096 -subj "/C=XX/ST=XX/L=XX/O=XX/OU=DOCKER/CN=${APP_NAME}" \
-keyout "${APP_ROOT}/ssl/default.key" \
-out "${APP_ROOT}/ssl/default.crt" \
-days 3650 -nodes -sha256 &> /dev/null
fi
if [ -z "${1}" ]; then
if [ ! -z ${NGINX_DYNAMIC_RELOAD} ]; then
eleven log info "enable dynamic reload"
/sbin/inotifyd /usr/local/bin/reload.sh ${APP_ROOT}/etc:cdnym &
fi
set -- "nginx" \
-g \
'daemon off;'
eleven log start
fi
exec "$@"

View File

@@ -1,26 +0,0 @@
#!/bin/ash
eleven log debug "inotifyd event: ${1}"
eleven log info "reloading config"
NGINX_DYNAMIC_RELOAD_LOG=${APP_ROOT}/run/reload.log
nginx -t &> ${NGINX_DYNAMIC_RELOAD_LOG}
while read -r LINE; do
if echo "${LINE}" | grep -q "nginx: "; then
if echo "${LINE}" | grep -q "\[warn\]"; then
LINE=$(echo ${LINE} | sed 's/nginx: \[warn\] //')
eleven log warning "${LINE}"
fi
if echo "${LINE}" | grep -q "\[emerg\]"; then
LINE=$(echo ${LINE} | sed 's/nginx: \[emerg\] //')
eleven log error "${LINE}"
fi
fi
done < ${NGINX_DYNAMIC_RELOAD_LOG}
if cat ${NGINX_DYNAMIC_RELOAD_LOG} | grep -q "test is successful"; then
nginx -s reload
eleven log info "config reloaded"
else
eleven log error "config reload failed!"
fi