├── .github ├── dependabot.yml └── workflows │ ├── dockerimage.yml │ ├── push-to-ghcr.yml │ └── stale-bot.yml ├── .gitignore ├── .hadolint.yaml ├── Dockerfile ├── nginx.conf ├── readme.md ├── run-docker.sh ├── ssl_common.conf └── tests ├── https.conf ├── index.html ├── localhost.crt ├── localhost.key ├── modules.conf ├── njs.conf ├── njs └── http.js ├── perl_rewrite.conf └── static.conf /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/dockerimage.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | jobs: 9 | 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v5 15 | 16 | # https://github.com/FiloSottile/mkcert#installation 17 | - name: Install and setup mkcert 18 | env: 19 | MKCERT_VERSION: v1.4.3 20 | run: | 21 | set -x 22 | sudo apt-get update && \ 23 | sudo apt-get install -y libnss3-tools 24 | 25 | curl https://github.com/FiloSottile/mkcert/releases/download/${MKCERT_VERSION}/mkcert-${MKCERT_VERSION}-linux-amd64 --location --output /tmp/mkcert 26 | chmod 744 /tmp/mkcert 27 | sudo mv /tmp/mkcert /bin/mkcert 28 | 29 | mkcert -install 30 | 31 | - name: Create signed TLS certificates for localhost 32 | run: | 33 | mkcert -cert-file tests/localhost.crt -key-file tests/localhost.key localhost 0.0.0.0 ::1 34 | 35 | # https://github.com/marketplace/actions/build-and-push-docker-images 36 | - name: Build the Docker image 37 | uses: docker/build-push-action@v6 38 | with: 39 | context: . 40 | tags: macbre/nginx 41 | cache-from: | 42 | ghcr.io/macbre/nginx-http3:latest 43 | 44 | - name: Lint the Dockerfile 45 | uses: hadolint/hadolint-action@v3.3.0 46 | with: 47 | dockerfile: Dockerfile 48 | 49 | - name: Inspect images 50 | run: | 51 | docker images | head -n3 52 | 53 | - name: Are we running as non-root? 54 | run: | 55 | docker run --rm -t macbre/nginx whoami | grep nginx 56 | 57 | - name: Run nginx -V and njs -v 58 | run: | 59 | docker run --rm -t macbre/nginx nginx -V | sed 's/\-\-/\n\t--/g' | tee 60 | echo "njs v$(docker run -t macbre/nginx njs -v)" 61 | 62 | - name: Serve a static asset 63 | run: | 64 | # expand commands 65 | set -x 66 | 67 | ./run-docker.sh & 68 | 69 | sleep 2; docker ps 70 | curl -v --compressed localhost:8888 2>&1 | tee /tmp/out 71 | 72 | grep --fixed-strings --invert-match -i '< Server: nginx' /tmp/out > /dev/null 73 | grep --fixed-strings '< Content-Encoding: zstd' /tmp/out 74 | grep --fixed-strings '
It works!
' /tmp/out 75 | 76 | 77 | curl -v --compressed localhost:8888/FooBar 2>&1 | tee /tmp/out 78 | 79 | grep --fixed-strings 'HTTP/1.1 301 Moved Permanently' /tmp/out 80 | grep --fixed-strings '< x-rewrite: 1' /tmp/out 81 | grep --fixed-strings '< Location: http://localhost/foobar' /tmp/out 82 | 83 | - name: Check njs module 84 | run: | 85 | set -x 86 | 87 | curl -v --compressed -H 'Host: njs' localhost:8888/hello 2>&1 | tee /tmp/out 88 | grep --fixed-strings '< x-njs: 1' /tmp/out 89 | grep 'Hello world from njs' /tmp/out 90 | 91 | - name: http2 vs h3 92 | run: | 93 | set -x 94 | 95 | curl -v --compressed https://localhost:8889 2>&1 | tee /tmp/h2 96 | 97 | grep --fixed-strings '< HTTP/2 200' /tmp/h2 98 | grep --fixed-strings --invert-match -i '< server: nginx' /tmp/h2 > /dev/null 99 | grep --fixed-strings 'It works!
' /tmp/h2 100 | 101 | 102 | docker run --rm --network host ghcr.io/macbre/curl-http3 \ 103 | curl -v --insecure https://localhost:8889 --http3 --max-time 5 2>&1 | tee /tmp/h3 104 | 105 | grep --fixed-strings 'HTTP/3 200' /tmp/h3 106 | grep --fixed-strings --invert-match -i '< server: nginx' /tmp/h3 > /dev/null 107 | grep --fixed-strings '< alt-svc: h3=":8889"; ma=86400' /tmp/h3 108 | grep --fixed-strings '< quic-status: h3' /tmp/h3 109 | grep --fixed-strings 'It works!
' /tmp/h3 110 | 111 | - name: Test njs command line 112 | run: | 113 | echo "console.log('Using njs v' + njs.version)" | docker run -i --rm macbre/nginx njs -q - | grep "Using njs v0.9.3" 114 | 115 | - name: Show logs 116 | if: always() 117 | run: docker logs test_nginx 118 | -------------------------------------------------------------------------------- /.github/workflows/push-to-ghcr.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish a Docker image to ghcr.io and Docker Hub 2 | on: 3 | 4 | # publish on releases (tagged as "x.y.z" - "v" prefix is removed) 5 | release: 6 | types: [ published ] 7 | 8 | # publish on pushes to the main branch (tagged as "master") 9 | push: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | docker_publish: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v5 19 | 20 | # https://github.com/marketplace/actions/push-to-ghcr 21 | - name: Build and publish a Docker image for macbre/nginx-http3 22 | uses: macbre/push-to-ghcr@v16 23 | with: 24 | image_name: macbre/nginx-http3 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | docker_io_token: ${{ secrets.DOCKER_IO_ACCESS_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/stale-bot.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '0 1,13 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | # https://github.com/actions/stale 11 | - uses: actions/stale@v10 12 | with: 13 | # global config 14 | operations-per-run: 300 15 | days-before-stale: 30 16 | days-before-close: 90 17 | # issue specific config 18 | stale-issue-label: 'wontfix' 19 | stale-issue-message: > 20 | This issue has been automatically marked as stale because it has not had 21 | recent activity. It will be closed if no further activity occurs. Thank you 22 | for your contributions. 23 | close-issue-message: 'Closing this due to being stale.' 24 | 25 | # https://github.com/actions/stale?tab=readme-ov-file#exempt-issue-labels 26 | exempt-issue-labels: 'keep-open' 27 | 28 | # pull request specific config 29 | 30 | exempt-pr-labels: 'dependencies' 31 | stale-pr-label: 'wontfix' 32 | stale-pr-message: > 33 | This pull request has been automatically marked as stale because it has not had 34 | recent activity. It will be closed if no further activity occurs. Thank you 35 | for your contributions. 36 | close-pr-message: 'Closing this due to being stale.' 37 | delete-branch: true 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /.hadolint.yaml: -------------------------------------------------------------------------------- 1 | strict-labels: true 2 | ignored: 3 | - DL3003 # Use WORKDIR to switch to a directory 4 | - DL3018 # Pin versions in apk add. 5 | - DL4006 # Set the SHELL option -o pipefail before RUN with a pipe in it. 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # https://hg.nginx.org/nginx/file/tip/src/core/nginx.h 2 | ARG NGINX_VERSION=1.29.2 3 | 4 | # https://hg.nginx.org/nginx/ 5 | ARG NGINX_COMMIT=d2a04c09eb4d 6 | 7 | # https://github.com/google/ngx_brotli 8 | ARG NGX_BROTLI_COMMIT=a71f9312c2deb28875acc7bacfdd5695a111aa53 9 | 10 | # https://github.com/google/boringssl 11 | #ARG BORINGSSL_COMMIT=fae0964b3d44e94ca2a2d21f86e61dabe683d130 12 | 13 | # https://github.com/nginx/njs/releases/tag/0.9.3 14 | ARG NJS_COMMIT=5115c78ea0259fa7ef42b629eda4964fbf63aba5 15 | 16 | # https://github.com/openresty/headers-more-nginx-module#installation 17 | # we want to have https://github.com/openresty/headers-more-nginx-module/commit/e536bc595d8b490dbc9cf5999ec48fca3f488632 18 | ARG HEADERS_MORE_VERSION=0.38 19 | 20 | # https://github.com/leev/ngx_http_geoip2_module/releases 21 | ARG GEOIP2_VERSION=3.4 22 | 23 | # https://github.com/tokers/zstd-nginx-module/releases 24 | ARG ZSTD_VERSION=0.1.1 25 | 26 | # NGINX UID / GID 27 | ARG NGINX_USER_UID=100 28 | ARG NGINX_GROUP_GID=101 29 | 30 | # https://nginx.org/en/docs/http/ngx_http_v3_module.html 31 | # https://nginx.org/en/docs/configure.html 32 | ARG CONFIG="\ 33 | --build=$NGINX_COMMIT \ 34 | --prefix=/etc/nginx \ 35 | --sbin-path=/usr/sbin/nginx \ 36 | --modules-path=/usr/lib/nginx/modules \ 37 | --conf-path=/etc/nginx/nginx.conf \ 38 | --error-log-path=/var/log/nginx/error.log \ 39 | --http-log-path=/var/log/nginx/access.log \ 40 | --pid-path=/var/run/nginx/nginx.pid \ 41 | --lock-path=/var/run/nginx/nginx.lock \ 42 | --http-client-body-temp-path=/var/cache/nginx/client_temp \ 43 | --http-proxy-temp-path=/var/cache/nginx/proxy_temp \ 44 | --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \ 45 | --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \ 46 | --http-scgi-temp-path=/var/cache/nginx/scgi_temp \ 47 | --user=nginx \ 48 | --group=nginx \ 49 | --with-http_ssl_module \ 50 | --with-http_realip_module \ 51 | --with-http_addition_module \ 52 | --with-http_sub_module \ 53 | --with-http_dav_module \ 54 | --with-http_flv_module \ 55 | --with-http_mp4_module \ 56 | --with-http_gunzip_module \ 57 | --with-http_gzip_static_module \ 58 | --with-http_random_index_module \ 59 | --with-http_secure_link_module \ 60 | --with-http_stub_status_module \ 61 | --with-http_auth_request_module \ 62 | --with-http_xslt_module=dynamic \ 63 | --with-http_image_filter_module=dynamic \ 64 | --with-http_geoip_module=dynamic \ 65 | --with-http_perl_module=dynamic \ 66 | --with-threads \ 67 | --with-stream \ 68 | --with-stream_ssl_module \ 69 | --with-stream_ssl_preread_module \ 70 | --with-stream_realip_module \ 71 | --with-stream_geoip_module=dynamic \ 72 | --with-http_slice_module \ 73 | --with-mail \ 74 | --with-mail_ssl_module \ 75 | --with-compat \ 76 | --with-file-aio \ 77 | --with-http_v2_module \ 78 | --with-http_v3_module \ 79 | --with-openssl-opt=enable-ktls \ 80 | --add-module=/usr/src/ngx_brotli \ 81 | --add-module=/usr/src/headers-more-nginx-module-$HEADERS_MORE_VERSION \ 82 | --add-module=/usr/src/njs/nginx \ 83 | --add-module=/usr/src/zstd \ 84 | --add-dynamic-module=/usr/src/ngx_http_geoip2_module \ 85 | " 86 | 87 | FROM alpine:3.20 AS base 88 | 89 | ARG NGINX_VERSION 90 | ARG NGINX_COMMIT 91 | ARG NGX_BROTLI_COMMIT 92 | ARG HEADERS_MORE_VERSION 93 | ARG NJS_COMMIT 94 | ARG GEOIP2_VERSION 95 | ARG ZSTD_VERSION 96 | ARG NGINX_USER_UID 97 | ARG NGINX_GROUP_GID 98 | ARG CONFIG 99 | 100 | RUN \ 101 | apk add --no-cache --virtual .build-deps \ 102 | gcc \ 103 | gd-dev \ 104 | geoip-dev \ 105 | gnupg \ 106 | go \ 107 | libc-dev \ 108 | libxslt-dev \ 109 | linux-headers \ 110 | make \ 111 | mercurial \ 112 | musl-dev \ 113 | ninja \ 114 | openssl-dev \ 115 | pcre-dev \ 116 | perl-dev \ 117 | zlib-dev \ 118 | && apk add --no-cache --virtual .brotli-build-deps \ 119 | autoconf \ 120 | automake \ 121 | cmake \ 122 | g++ \ 123 | git \ 124 | libtool \ 125 | && apk add --no-cache --virtual .geoip2-build-deps \ 126 | libmaxminddb-dev \ 127 | && apk add --no-cache --virtual .njs-build-deps \ 128 | libedit-dev \ 129 | libxml2-dev \ 130 | libxslt-dev \ 131 | openssl-dev \ 132 | pcre-dev \ 133 | readline-dev \ 134 | zlib-dev \ 135 | && apk add --no-cache --virtual .zstd-build-deps \ 136 | zstd-dev \ 137 | && git config --global init.defaultBranch master 138 | 139 | WORKDIR /usr/src/ 140 | 141 | RUN \ 142 | echo "Cloning nginx $NGINX_VERSION (rev $NGINX_COMMIT from 'default' branch) ..." \ 143 | && hg clone -b default --rev $NGINX_COMMIT https://hg.nginx.org/nginx/ /usr/src/nginx-$NGINX_VERSION 144 | 145 | RUN \ 146 | echo "Cloning brotli $NGX_BROTLI_COMMIT ..." \ 147 | && mkdir /usr/src/ngx_brotli \ 148 | && cd /usr/src/ngx_brotli \ 149 | && git init \ 150 | && git remote add origin https://github.com/google/ngx_brotli.git \ 151 | && git fetch --depth 1 origin $NGX_BROTLI_COMMIT \ 152 | && git checkout --recurse-submodules -q FETCH_HEAD \ 153 | && git submodule update --init --depth 1 154 | 155 | # hadolint ignore=SC2086 156 | #RUN \ 157 | # echo "Cloning boringssl ..." \ 158 | # && cd /usr/src \ 159 | # && git clone https://github.com/google/boringssl \ 160 | # && cd boringssl \ 161 | # && git checkout $BORINGSSL_COMMIT 162 | 163 | #RUN \ 164 | # echo "Building boringssl ..." \ 165 | # && cd /usr/src/boringssl \ 166 | # && mkdir build \ 167 | # && cd build \ 168 | # && cmake -GNinja .. \ 169 | # && ninja 170 | 171 | RUN \ 172 | echo "Downloading headers-more-nginx-module ..." \ 173 | && cd /usr/src \ 174 | && wget -q https://github.com/openresty/headers-more-nginx-module/archive/refs/tags/v${HEADERS_MORE_VERSION}.tar.gz -O headers-more-nginx-module.tar.gz \ 175 | && tar -xf headers-more-nginx-module.tar.gz 176 | 177 | RUN \ 178 | echo "Downloading ngx_http_geoip2_module ..." \ 179 | && git clone --depth 1 --branch ${GEOIP2_VERSION} https://github.com/leev/ngx_http_geoip2_module /usr/src/ngx_http_geoip2_module 180 | 181 | RUN \ 182 | echo "Downloading zstd-nginx-module ..." \ 183 | && git clone --depth 1 --branch ${ZSTD_VERSION} https://github.com/tokers/zstd-nginx-module.git /usr/src/zstd 184 | 185 | RUN \ 186 | echo "Cloning and configuring quickjs ..." \ 187 | && cd /usr/src \ 188 | && git clone https://github.com/bellard/quickjs quickjs \ 189 | && cd quickjs \ 190 | && make libquickjs.a \ 191 | && echo "quickjs $(cat VERSION)" 192 | 193 | RUN \ 194 | echo "Cloning and configuring njs ..." \ 195 | && mkdir /usr/src/njs && cd /usr/src/njs \ 196 | && git init \ 197 | && git remote add origin https://github.com/nginx/njs.git \ 198 | && git fetch --depth 1 origin ${NJS_COMMIT} \ 199 | && git checkout -q FETCH_HEAD \ 200 | && ./configure --cc-opt='-I /usr/src/quickjs' --ld-opt="-L /usr/src/quickjs" \ 201 | && make njs \ 202 | && mv /usr/src/njs/build/njs /usr/sbin/njs \ 203 | && echo "njs v$(njs -v)" 204 | 205 | # https://github.com/macbre/docker-nginx-http3/issues/152 206 | ARG CC_OPT='-g -O2 -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -I /usr/src/quickjs' 207 | ARG LD_OPT='-Wl,-Bsymbolic-functions -flto=auto -ffat-lto-objects -flto=auto -L /usr/src/quickjs' 208 | RUN \ 209 | echo "Building nginx ..." \ 210 | && mkdir -p /var/run/nginx/ \ 211 | && cd /usr/src/nginx-$NGINX_VERSION \ 212 | && ./auto/configure $CONFIG --with-cc-opt="$CC_OPT" --with-ld-opt="$LD_OPT" \ 213 | && make -j"$(getconf _NPROCESSORS_ONLN)" 214 | 215 | RUN \ 216 | cd /usr/src/nginx-$NGINX_VERSION \ 217 | && make install \ 218 | && rm -rf /etc/nginx/html/ \ 219 | && mkdir /etc/nginx/conf.d/ \ 220 | && strip /usr/sbin/nginx* \ 221 | && strip /usr/lib/nginx/modules/*.so \ 222 | \ 223 | # https://tools.ietf.org/html/rfc7919 224 | # https://github.com/mozilla/ssl-config-generator/blob/master/docs/ffdhe2048.txt 225 | && wget -q https://ssl-config.mozilla.org/ffdhe2048.txt -O /etc/ssl/dhparam.pem \ 226 | \ 227 | # Bring in gettext so we can get `envsubst`, then throw 228 | # the rest away. To do this, we need to install `gettext` 229 | # then move `envsubst` out of the way so `gettext` can 230 | # be deleted completely, then move `envsubst` back. 231 | && apk add --no-cache --virtual .gettext gettext \ 232 | \ 233 | && scanelf --needed --nobanner /usr/sbin/nginx /usr/sbin/njs /usr/lib/nginx/modules/*.so /usr/bin/envsubst \ 234 | | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ 235 | | sort -u \ 236 | | xargs -r apk info --installed \ 237 | | sort -u > /tmp/runDeps.txt 238 | 239 | FROM alpine:3.20 240 | ARG NGINX_VERSION 241 | ARG NGINX_COMMIT 242 | ARG NGINX_USER_UID 243 | ARG NGINX_GROUP_GID 244 | 245 | ENV NGINX_VERSION=$NGINX_VERSION 246 | ENV NGINX_COMMIT=$NGINX_COMMIT 247 | 248 | COPY --from=base /var/run/nginx/ /var/run/nginx/ 249 | COPY --from=base /tmp/runDeps.txt /tmp/runDeps.txt 250 | COPY --from=base /etc/nginx /etc/nginx 251 | COPY --from=base /usr/lib/nginx/modules/*.so /usr/lib/nginx/modules/ 252 | COPY --from=base /usr/sbin/nginx /usr/sbin/ 253 | COPY --from=base /usr/local/lib/perl5/site_perl /usr/local/lib/perl5/site_perl 254 | COPY --from=base /usr/bin/envsubst /usr/local/bin/envsubst 255 | COPY --from=base /etc/ssl/dhparam.pem /etc/ssl/dhparam.pem 256 | 257 | COPY --from=base /usr/sbin/njs /usr/sbin/njs 258 | 259 | # hadolint ignore=SC2046 260 | RUN \ 261 | addgroup --gid $NGINX_GROUP_GID -S nginx \ 262 | && adduser --uid $NGINX_USER_UID -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx \ 263 | && apk add --no-cache --virtual .nginx-rundeps tzdata $(cat /tmp/runDeps.txt) \ 264 | && rm /tmp/runDeps.txt \ 265 | && ln -s /usr/lib/nginx/modules /etc/nginx/modules \ 266 | # forward request and error logs to docker log collector 267 | && mkdir /var/log/nginx \ 268 | && touch /var/log/nginx/access.log /var/log/nginx/error.log \ 269 | && ln -sf /dev/stdout /var/log/nginx/access.log \ 270 | && ln -sf /dev/stderr /var/log/nginx/error.log 271 | 272 | COPY nginx.conf /etc/nginx/nginx.conf 273 | COPY ssl_common.conf /etc/nginx/conf.d/ssl_common.conf 274 | 275 | # show env 276 | RUN env | sort 277 | 278 | # njs version 279 | RUN njs -v 280 | 281 | # test the configuration 282 | RUN nginx -V; nginx -t 283 | 284 | EXPOSE 8080 8443 285 | 286 | STOPSIGNAL SIGTERM 287 | 288 | # prepare to switching to non-root - update file permissions of directory containing 289 | # nginx.lock and nginx.pid file 290 | RUN \ 291 | chown -R --verbose nginx:nginx \ 292 | /var/run/nginx/ 293 | 294 | USER nginx 295 | CMD ["nginx", "-g", "daemon off;"] 296 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | # this allows you to call directives such as "env" in your own conf files 2 | # http://nginx.org/en/docs/ngx_core_module.html#env 3 | # 4 | # and load dynamic modules via load_module 5 | # http://nginx.org/en/docs/ngx_core_module.html#load_module 6 | include /etc/nginx/main.d/*.conf; 7 | 8 | worker_processes 1; 9 | 10 | error_log /var/log/nginx/error.log warn; 11 | pid /var/run/nginx/nginx.pid; 12 | 13 | events { 14 | worker_connections 1024; 15 | } 16 | 17 | 18 | http { 19 | include /etc/nginx/mime.types; 20 | default_type application/octet-stream; 21 | 22 | log_format quic '$remote_addr - $remote_user [$time_local] "$request" ' 23 | '$status $body_bytes_sent "$http_referer" ' 24 | '"$http_user_agent" "$http_x_forwarded_for" "$http3"'; 25 | 26 | access_log /var/log/nginx/access.log quic; 27 | 28 | sendfile on; 29 | #tcp_nopush on; 30 | 31 | keepalive_timeout 65; 32 | 33 | # security, reveal less information about ourselves 34 | server_tokens off; # disables emitting nginx version in error messages and in the “Server” response header field 35 | more_clear_headers 'Server'; 36 | more_clear_headers 'X-Powered-By'; 37 | 38 | # prevent clickjacking attacks 39 | more_set_headers 'X-Frame-Options: SAMEORIGIN'; 40 | 41 | # help to prevent cross-site scripting exploits 42 | more_set_headers 'X-XSS-Protection: 1; mode=block'; 43 | 44 | # help to prevent Cross-Site Scripting (XSS) and data injection attacks 45 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP 46 | more_set_headers "Content-Security-Policy: object-src 'none'; frame-ancestors 'self'; form-action 'self'; block-all-mixed-content; sandbox allow-forms allow-same-origin allow-scripts allow-popups allow-downloads; base-uri 'self';"; 47 | 48 | # enable response compression 49 | gzip on; 50 | 51 | # https://github.com/google/ngx_brotli#configuration-directives 52 | brotli on; 53 | brotli_static on; 54 | 55 | # https://github.com/tokers/zstd-nginx-module#directives 56 | zstd on; 57 | zstd_static on; 58 | 59 | include /etc/nginx/conf.d/*.conf; 60 | } 61 | 62 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## What is this? 2 | [](https://github.com/macbre/docker-nginx-http3/actions/workflows/dockerimage.yml) 3 | 4 | Stable and up-to-date [nginx](https://nginx.org/en/CHANGES) with [QUIC + HTTP/3 support](https://nginx.org/en/docs/http/ngx_http_v3_module.html), [Google's `brotli` compression](https://github.com/google/ngx_brotli), [`zstd` compression](https://github.com/tokers/zstd-nginx-module), [`njs` module](https://nginx.org/en/docs/njs/), [kTLS/sendfile support](https://delthas.fr/blog/2023/kernel-tls/) and [Grade A+ SSL config](https://ssl-config.mozilla.org/) 5 | 6 | ## How to use this image 7 | As this project is based on the official [nginx image](https://hub.docker.com/_/nginx/) look for instructions there. In addition to the standard configuration directives, you'll be able to use the brotli module specific ones, see [here for official documentation](https://github.com/google/ngx_brotli#configuration-directives) 8 | 9 | ``` 10 | docker pull macbre/nginx-http3:latest 11 | ``` 12 | 13 | You can fetch an image from [Github Containers Registry](https://github.com/macbre/docker-nginx-brotli/pkgs/container/nginx-http3) as well: 14 | 15 | ``` 16 | docker pull ghcr.io/macbre/nginx-http3:latest 17 | ``` 18 | 19 | ## What's inside 20 | 21 | * [built-in nginx modules](https://nginx.org/en/docs/) 22 | * [`headers-more-nginx-module`](https://github.com/openresty/headers-more-nginx-module#readme) - sets and clears HTTP request and response headers 23 | * [`ngx_brotli`](https://github.com/google/ngx_brotli#configuration-directives) - adds [brotli response compression](https://datatracker.ietf.org/doc/html/rfc7932) 24 | * [`zstd-nginx-module`](https://github.com/tokers/zstd-nginx-module#directives) - adds [Zstandard response compression](https://datatracker.ietf.org/doc/html/rfc8878) 25 | * [`ngx_http_geoip2_module`](https://github.com/leev/ngx_http_geoip2_module#download-maxmind-geolite2-database-optional) - creates variables with values from the maxmind geoip2 databases based on the client IP 26 | * [`njs` module](https://nginx.org/en/docs/njs/) - a subset of the JavaScript language that allows extending nginx functionality ([GitHub repository](https://github.com/nginx/njs)) 27 | 28 | ``` 29 | $ docker run -it macbre/nginx-http3 nginx -V 30 | nginx version: nginx/1.29.2 (d2a04c09eb4d) 31 | built by gcc 13.2.1 20240309 (Alpine 13.2.1_git20240309) 32 | built with OpenSSL 3.3.5 30 Sep 2025 (running with OpenSSL 3.3.4 1 Jul 2025) 33 | TLS SNI support enabled 34 | configure arguments: 35 | --build=d2a04c09eb4d 36 | --prefix=/etc/nginx 37 | --sbin-path=/usr/sbin/nginx 38 | --modules-path=/usr/lib/nginx/modules 39 | --conf-path=/etc/nginx/nginx.conf 40 | --error-log-path=/var/log/nginx/error.log 41 | --http-log-path=/var/log/nginx/access.log 42 | --pid-path=/var/run/nginx/nginx.pid 43 | --lock-path=/var/run/nginx/nginx.lock 44 | --http-client-body-temp-path=/var/cache/nginx/client_temp 45 | --http-proxy-temp-path=/var/cache/nginx/proxy_temp 46 | --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp 47 | --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp 48 | --http-scgi-temp-path=/var/cache/nginx/scgi_temp 49 | --user=nginx 50 | --group=nginx 51 | --with-http_ssl_module 52 | --with-http_realip_module 53 | --with-http_addition_module 54 | --with-http_sub_module 55 | --with-http_dav_module 56 | --with-http_flv_module 57 | --with-http_mp4_module 58 | --with-http_gunzip_module 59 | --with-http_gzip_static_module 60 | --with-http_random_index_module 61 | --with-http_secure_link_module 62 | --with-http_stub_status_module 63 | --with-http_auth_request_module 64 | --with-http_xslt_module=dynamic 65 | --with-http_image_filter_module=dynamic 66 | --with-http_geoip_module=dynamic 67 | --with-http_perl_module=dynamic 68 | --with-threads 69 | --with-stream 70 | --with-stream_ssl_module 71 | --with-stream_ssl_preread_module 72 | --with-stream_realip_module 73 | --with-stream_geoip_module=dynamic 74 | --with-http_slice_module 75 | --with-mail 76 | --with-mail_ssl_module 77 | --with-compat 78 | --with-file-aio 79 | --with-http_v2_module 80 | --with-http_v3_module 81 | --with-openssl-opt=enable-ktls 82 | --add-module=/usr/src/ngx_brotli 83 | --add-module=/usr/src/headers-more-nginx-module-0.38 84 | --add-module=/usr/src/njs/nginx 85 | --add-module=/usr/src/zstd 86 | --add-dynamic-module=/usr/src/ngx_http_geoip2_module 87 | --with-cc-opt='-g -O2 -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -I /usr/src/quickjs' 88 | --with-ld-opt='-Wl,-Bsymbolic-functions -flto=auto -ffat-lto-objects -flto=auto -L /usr/src/quickjs' 89 | 90 | 91 | $ docker run -it macbre/nginx-http3 njs -v 92 | 0.9.3 93 | ``` 94 | 95 | ## SSL Grade A+ handling 96 | 97 | Please refer to [Mozilla's SSL Configuration Generator](https://ssl-config.mozilla.org/). This image has `https://ssl-config.mozilla.org/ffdhe2048.txt` DH parameters for DHE ciphers fetched and stored in `/etc/ssl/dhparam.pem`: 98 | 99 | ``` 100 | ssl_dhparam /etc/ssl/dhparam.pem; 101 | ``` 102 | 103 | See [ssllabs.com test results for wbc.macbre.net](https://www.ssllabs.com/ssltest/analyze.html?d=wbc.macbre.net). 104 | 105 | ## nginx config files includes 106 | 107 | * `.conf` files mounted in `/etc/nginx/main.d` will be included in the `main` nginx context (e.g. you can call [`env` directive](http://nginx.org/en/docs/ngx_core_module.html#env) there) 108 | * `.conf` files mounted in `/etc/nginx/conf.d` will be included in the `http` nginx context 109 | 110 | ## QUIC + HTTP/3 support 111 | 112 |It works!
3 | -------------------------------------------------------------------------------- /tests/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC5TCCAc2gAwIBAgIJAMTIcHZxspojMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0yMTA1MTgxMzU2MjhaFw0yMTA2MTcxMzU2MjhaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAMZ8LqQkwnAFF7GZkux4Fz0EQrzzRTGLC9cQABS93gHKJul9MlQ+APK3T7a8 6 | +HQJNrTxb3ZfwZ+vAwsfSgKmDp63AzdAkYNMb3ldGbKOUP12GsluTb+MV0f9ir3S 7 | Kcs3BC88TQMtotSOjb/aqqM7ziLtkpaw0Po+tMncrU3bpTisFv7QtExx32duNlhp 8 | yY5xSgCuK7cKPanIsfTZ36e2AsKBDuPZxqf5RoQ1Zx2VDmfqMfiOwlrsIksAJT4I 9 | YL5GHJYpwSC8Eox1nA9zohvBvo6EkWtuT6kBQFmo911ZEezul0I2DDvSscFqPU3x 10 | 5J3Btnxr0FkUEozM1HHXQBU7hcECAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo 11 | b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B 12 | AQsFAAOCAQEAIOpieZ1blUR0GTsG8DJlllBH4zAiF/DxPrU8totN3BGh71JQgN/R 13 | xWgEHMRiDRPDawMuHhvPI6mDe+BsHQNA9uNvAjHDo8tUHAADmhN2xvUnHIheUOpM 14 | 7AcIj+Gs5dRiO0LxcqS3LrGsg9ZdUFDBO9ABNDD2l6Inr/sj4OtItO4i7WsTbNr3 15 | EKF6AFAbTV0NhdUcQ4+2X3+fMEVshokW+Kytl5y96eVlvYP2hc3qXW9ZdNrq4xBC 16 | v5lTk73N/YsOVrcf9MV8lj6VHhp0G4DxgrW8qIc6lI5y7nRVgyjkYqN4V5NBJ8/I 17 | /F4n/NGKh05OYc+AaOcOGnQjYGBBsCjtfA== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /tests/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDGfC6kJMJwBRex 3 | mZLseBc9BEK880UxiwvXEAAUvd4ByibpfTJUPgDyt0+2vPh0CTa08W92X8GfrwML 4 | H0oCpg6etwM3QJGDTG95XRmyjlD9dhrJbk2/jFdH/Yq90inLNwQvPE0DLaLUjo2/ 5 | 2qqjO84i7ZKWsND6PrTJ3K1N26U4rBb+0LRMcd9nbjZYacmOcUoAriu3Cj2pyLH0 6 | 2d+ntgLCgQ7j2can+UaENWcdlQ5n6jH4jsJa7CJLACU+CGC+RhyWKcEgvBKMdZwP 7 | c6Ibwb6OhJFrbk+pAUBZqPddWRHs7pdCNgw70rHBaj1N8eSdwbZ8a9BZFBKMzNRx 8 | 10AVO4XBAgMBAAECggEBAKtvBOX3bg1NCLr390PRMdkcvfvvblstSsu1YSyJFaCc 9 | FuHipb7HUxBQrkQakcm/T4YXZntql5Uxl0JbAxSoksYZMmjdlxE9yqabeB+V9nQF 10 | N3u1jueb8qMWZWgwhrKj0TRvM+FD4HkQq3QDDREyDYhzhAIttEIAOSyFMxalWN5J 11 | lM3rlNU9ATy/7Zv+0fbZVyp4yIvXToGSwSEYJlhhsfS77by16mDgu8ANNL7k14Vw 12 | luogmLZ51VTo0ppT0UI3AjxnA/hG+K5SW95ySI8NWJEeGNAhu8A1bIjs9K6D7GVq 13 | LfazaT4dZH4GuwH0bfvUlrmi0GYNilcePIijjpHx4HECgYEA/nH53s6C4jNw222w 14 | 4IntSM18Cc9e8TDzuGsVEmC/Fyo50NU0XT+qSKP6/Bx6bpNe9VaMG+nWYP31rncc 15 | 6GECKIyW13+Z5DZCJbhFZYsErp5yZkj16paz1MOifgYs+FXYd3FT7y9Zp8+x67ap 16 | Wuq1x3RqdD2IQDdEy7tHEk8fPCUCgYEAx7KrMmG8bwO/Jm2k6oeYrWJSO8oQqC5i 17 | fw8C7ABKSISjJd4ci3s+ehpPzl6E/2+ixnRzw81LDMZUuORL6CKLD0CadYd8fgux 18 | B9B60AyifUcfdexflaO+L+i+0M8z+WIpJP2E8bz8XMlnoaRPlr7vEXOXGz2yRU3K 19 | aKj1epeKIm0CgYEA3AjwTg9D/B0EAZ2wEdz0x7p7A3jC38y3VsmzZTsoxP2NFdzp 20 | 9aGyvjKgPxjZf3oN6he3/gOPkjzMEt2KsCID3dEGOMFt8X5CYaAi0xcPK3p176I0 21 | l3NPfDTZ9iWdCfwiv9fO/85wt7/SWsaRWcATBrLTxEXZ575jzdLcOScXGckCgYEA 22 | tp2ptnHwGmkHtUGRDTOgp/WAk9Jttw5r3htBJcYCKBy7ARcDbX3vnUNQdbyzzM8u 23 | wGKfto+WsQDxWv7Sd16XYgRG+3FBpBZW9nlsxbK4KO4QVAsrQbEya6dgT50bv1NH 24 | ot7/YvzanpNDZGrYqVbDUmcs6Klby8qebWUMzuWjWWUCgYAUETANVSJjoHT87Rm3 25 | qEsSSpy1vcdTj/F0GiG390bjXZLaLWK11mjUf0KmLD43MnA4hT3VwC8n+3kAapaS 26 | Jac1k0Ed1sqlj7ivqviV2UFYK9hdSDKD4oqzy5tkjgQ1kB0KHT0Lpi5iUbPH/6gx 27 | q9zVwmZ0CYpfE9Qw6I+YEpG2cA== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/modules.conf: -------------------------------------------------------------------------------- 1 | load_module modules/ngx_http_perl_module.so; 2 | load_module modules/ngx_http_geoip2_module.so; 3 | -------------------------------------------------------------------------------- /tests/njs.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name njs; 4 | 5 | js_path /opt/njs; 6 | js_import http.js; 7 | 8 | location /hello { 9 | js_content http.hello; # hello() function from http.js will be called here 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/njs/http.js: -------------------------------------------------------------------------------- 1 | // @see https://nginx.org/en/docs/njs/ 2 | // @see https://nginx.org/en/docs/njs/reference.html 3 | function hello(r) { 4 | r.headersOut['x-njs'] = '1'; 5 | r.return(200, `Hello world from njs v${njs.version}\n`); 6 | } 7 | 8 | export default {hello}; 9 | -------------------------------------------------------------------------------- /tests/perl_rewrite.conf: -------------------------------------------------------------------------------- 1 | # https://www.rewriteguide.com/nginx-enforce-lower-case-urls/ 2 | perl_set $uri_lowercase 'sub { 3 | my $r = shift; 4 | my $uri = $r->uri; 5 | $uri = lc($uri); 6 | return $uri; 7 | }'; 8 | -------------------------------------------------------------------------------- /tests/static.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | root /static; 7 | 8 | gzip_static on; 9 | brotli_static on; 10 | 11 | expires 1d; 12 | } 13 | 14 | location ~ [A-Z] { 15 | add_header x-rewrite 1; 16 | rewrite ^(.*)$ $scheme://$host$uri_lowercase permanent; 17 | } 18 | } 19 | --------------------------------------------------------------------------------