├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── docker.yml ├── LICENSE ├── Makefile ├── README.md ├── default.conf ├── docker-compose.yml ├── docker └── Dockerfile ├── images └── 26946324088_5b3f0b1464_o.png ├── module.conf ├── nginx.conf └── run.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.paypal.me/appleboy46'] 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - "master" 12 | 13 | jobs: 14 | build-docker: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | 28 | - name: Login to GitHub Container Registry 29 | uses: docker/login-action@v3 30 | with: 31 | registry: ghcr.io 32 | username: ${{ github.repository_owner }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Docker meta 36 | id: docker-meta 37 | uses: docker/metadata-action@v5 38 | with: 39 | images: | 40 | ghcr.io/${{ github.repository }} 41 | tags: | 42 | type=raw,value=latest,enable={{is_default_branch}} 43 | type=semver,pattern={{version}} 44 | type=semver,pattern={{major}}.{{minor}} 45 | type=semver,pattern={{major}} 46 | 47 | - name: Build and push 48 | uses: docker/build-push-action@v5 49 | with: 50 | context: . 51 | platforms: linux/amd64,linux/arm,linux/arm64 52 | file: docker/Dockerfile 53 | push: ${{ github.event_name != 'pull_request' }} 54 | tags: ${{ steps.docker-meta.outputs.tags }} 55 | labels: ${{ steps.docker-meta.outputs.labels }} 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bo-Yi Wu 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker build -t appleboy/nginx-image-resizer . 3 | 4 | up: 5 | docker-compose $@ 6 | 7 | down: 8 | docker-compose $@ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nginx-image-resizer 2 | 3 | ![./images/26946324088_5b3f0b1464_o.png](./images/26946324088_5b3f0b1464_o.png) 4 | 5 | [![Docker Image](https://github.com/appleboy/nginx-image-resizer/actions/workflows/docker.yml/badge.svg?branch=master)](https://github.com/appleboy/nginx-image-resizer/actions/workflows/docker.yml) 6 | 7 | Nginx Image Resizer is a Docker-based tool that uses Nginx and ImageMagick to dynamically resize images. When you run this Docker image, you can specify the size of the image through URL parameters, and Nginx Image Resizer will return the resized image. This tool is particularly useful for websites and applications that need to dynamically adjust image sizes. 8 | 9 | ## Build Image 10 | 11 | ```bash 12 | docker build -t ghcr.io/appleboy/nginx-image-resizer . 13 | ``` 14 | 15 | ## RUN Image 16 | 17 | ```bash 18 | docker run --restart always \ 19 | -p 8002:80 \ 20 | -e NGINX_HOST=localhost \ 21 | -e IMAGE_HOST="http://localhost:9000" \ 22 | ghcr.io/appleboy/nginx-image-resizer 23 | ``` 24 | 25 | ## Paramemter 26 | 27 | | Parameter | required | default value | 28 | |-------------|--------|----| 29 | | NGINX_HOST | true | | 30 | | IMAGE_HOST | true | | 31 | | JPG_QUALITY | false | 95 | 32 | | EXPIRE_TIME | false | 7d | 33 | | MEMORY_SIZE | false | 20m | 34 | | CACHE_NAME | false | cache_zone | 35 | | CACHE_SIZE | false | 40m | 36 | | INACTIVE_TIME | false | 30d | 37 | 38 | ## example 39 | 40 | Start [minio](https://min.io/) and nginx-image-resizer using docker-compose.yml 41 | 42 | ```sh 43 | docker-compose up -d 44 | ``` 45 | 46 | set minio environment as following 47 | 48 | ```sh 49 | MINIO_ROOT_USER: YOUR_MINIO_ROOT_USER 50 | MINIO_ROOT_PASSWORD: YOUR_MINIO_ROOT_PASSWORD 51 | ``` 52 | 53 | set bucket as public permission. 54 | 55 | ```sh 56 | mc config host add minio http://localhost:9000 MINIO_ACCESS_KEY MINIO_SECRET_KEY 57 | mc anonymous set public minio/test 58 | ``` 59 | 60 | open browser as following 61 | 62 | ```sh 63 | # format 1: http://localhost:8002/resize_image_width/bucket_name/image_name 64 | http://localhost:8002/300/test/test.png 65 | # format 2: http://localhost:8002/${image_width}x${image_height}/bucket_name/image_name 66 | http://localhost:8002/300x200/test/test.png 67 | ``` 68 | 69 | ## Benchmark 70 | 71 | without nginx proxy cache: 72 | 73 | ```sh 74 | echo "GET http://localhost:8002/310/test/26946324088_5b3f0b1464_o.png" | vegeta attack -rate=100 -connections=1 -duration=1s | tee results.bin | vegeta report 75 | Requests [total, rate] 100, 101.01 76 | Duration [total, attack, wait] 8.258454731s, 989.999ms, 7.268455731s 77 | Latencies [mean, 50, 95, 99, max] 3.937031678s, 4.079690985s, 6.958110121s, 7.205018428s, 7.268455731s 78 | Bytes In [total, mean] 4455500, 44555.00 79 | Bytes Out [total, mean] 0, 0.00 80 | Success [ratio] 100.00% 81 | Status Codes [code:count] 200:100 82 | Error Set: 83 | ``` 84 | 85 | with nginx proxy cache: 86 | 87 | ```sh 88 | echo "GET http://localhost:8002/310/test/26946324088_5b3f0b1464_o.png" | vegeta attack -rate=100 -connections=1 -duration=1s | tee results.bin | vegeta report 89 | Requests [total, rate] 100, 101.01 90 | Duration [total, attack, wait] 993.312255ms, 989.998ms, 3.314255ms 91 | Latencies [mean, 50, 95, 99, max] 3.717219ms, 3.05486ms, 8.891027ms, 12.488937ms, 12.520428ms 92 | Bytes In [total, mean] 4455500, 44555.00 93 | Bytes Out [total, mean] 0, 0.00 94 | Success [ratio] 100.00% 95 | Status Codes [code:count] 200:100 96 | Error Set: 97 | ``` 98 | 99 | ## Reference 100 | 101 | * [Nginx: Real time image resizing and caching](https://github.com/sergejmueller/sergejmueller.github.io/wiki/Nginx:-Real-time-image-resizing-and-caching) 102 | * [NGINX reverse proxy image resizing + AWS S3](https://medium.com/merapar/nginx-reverse-proxy-image-resizing-aws-cece1db5da01) 103 | * [Nginx dynamic image resizing with caching](https://stumbles.id.au/nginx-dynamic-image-resizing-with-caching.html) 104 | * [High‑Performance Caching with NGINX and NGINX Plus](https://www.nginx.com/blog/nginx-high-performance-caching/) 105 | * [NGINX and NGINX Plus Deliver Responsive Images Without the Headaches](https://www.nginx.com/blog/responsive-images-without-headaches-nginx-plus/) 106 | -------------------------------------------------------------------------------- /default.conf: -------------------------------------------------------------------------------- 1 | proxy_cache_path /data keys_zone=${CACHE_NAME}:${MEMORY_SIZE} inactive=${INACTIVE_TIME} max_size=${CACHE_SIZE}; 2 | 3 | server { 4 | listen 8888; 5 | allow 127.0.0.1; 6 | deny all; 7 | access_log /var/log/nginx/resizer.log main; 8 | 9 | # Clean up the headers going to and from S3. 10 | proxy_hide_header "x-amz-id-2"; 11 | proxy_hide_header "x-amz-request-id"; 12 | proxy_hide_header "x-amz-storage-class"; 13 | proxy_hide_header "Set-Cookie"; 14 | proxy_ignore_headers "Set-Cookie"; 15 | 16 | location ~ ^/([0-9]+)/(.*)$ { 17 | set $width $1; 18 | set $path $2; 19 | rewrite ^ /$path break; 20 | proxy_pass ${IMAGE_HOST}; 21 | image_filter resize $width -; 22 | image_filter_buffer 100M; 23 | image_filter_jpeg_quality ${JPG_QUALITY}; 24 | } 25 | 26 | location ~ ^/([0-9]+)x([0-9]+)/(.*)$ { 27 | set $width $1; 28 | set $height $2; 29 | set $path $3; 30 | rewrite ^ /$path break; 31 | proxy_pass ${IMAGE_HOST}; 32 | image_filter resize $width $height; 33 | image_filter_buffer 100M; 34 | image_filter_jpeg_quality ${JPG_QUALITY}; 35 | } 36 | } 37 | 38 | server { 39 | listen 80 default_server; 40 | server_name ${NGINX_HOST}; 41 | access_log /var/log/nginx/proxy.log main; 42 | 43 | add_header X-Cache-status $upstream_cache_status; 44 | location ~ ^/([0-9]+)/(.*)$ { 45 | set $width $1; 46 | set $path $2; 47 | rewrite ^ /$path break; 48 | proxy_pass http://127.0.0.1:8888/$width/$path; 49 | proxy_cache ${CACHE_NAME}; 50 | proxy_cache_valid 200 302 24h; 51 | proxy_cache_valid 404 1m; 52 | # expire time for browser 53 | expires ${EXPIRE_TIME}; 54 | } 55 | 56 | location ~ ^/([0-9]+)x([0-9]+)/(.*)$ { 57 | set $width $1; 58 | set $height $2; 59 | set $path $3; 60 | rewrite ^ /$path break; 61 | proxy_pass http://127.0.0.1:8888/${width}x${height}/$path; 62 | proxy_cache ${CACHE_NAME}; 63 | proxy_cache_valid 200 302 24h; 64 | proxy_cache_valid 404 1m; 65 | # expire time for browser 66 | expires ${EXPIRE_TIME}; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | minio: 5 | image: quay.io/minio/minio:RELEASE.2024-01-29T03-56-32Z 6 | container_name: minio 7 | ports: 8 | - "9000:9000" 9 | - "9001:9001" 10 | volumes: 11 | - minio-data:/data 12 | logging: 13 | driver: "json-file" 14 | options: 15 | max-size: "1k" 16 | max-file: "3" 17 | environment: 18 | MINIO_ROOT_USER: minio 19 | MINIO_ROOT_PASSWORD: test123456 20 | command: server /data --console-address ":9001" 21 | 22 | image-resizer: 23 | image: ghcr.io/appleboy/nginx-image-resizer:latest 24 | container_name: image-resizer 25 | ports: 26 | - 8002:80 27 | volumes: 28 | - cache-data:/data 29 | logging: 30 | driver: "json-file" 31 | options: 32 | max-size: "1k" 33 | max-file: "3" 34 | environment: 35 | IMAGE_HOST: http://minio:9000 36 | NGINX_HOST: localhost 37 | 38 | volumes: 39 | minio-data: 40 | cache-data: 41 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.15-alpine 2 | 3 | LABEL maintainer="Bo-Yi Wu " 4 | 5 | COPY default.conf /etc/nginx/conf.d/default.conf 6 | COPY module.conf /etc/nginx/module.conf 7 | COPY nginx.conf /etc/nginx/nginx.conf 8 | COPY run.sh /bin/ 9 | 10 | EXPOSE 80 11 | 12 | CMD ["/bin/run.sh"] 13 | -------------------------------------------------------------------------------- /images/26946324088_5b3f0b1464_o.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/nginx-image-resizer/3120c576071ee87f3e6853ef2b7facf59b01f27d/images/26946324088_5b3f0b1464_o.png -------------------------------------------------------------------------------- /module.conf: -------------------------------------------------------------------------------- 1 | load_module modules/ngx_http_image_filter_module.so; 2 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | include /etc/nginx/module.conf; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | 14 | http { 15 | include /etc/nginx/mime.types; 16 | default_type application/octet-stream; 17 | 18 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 19 | '$status $body_bytes_sent "$http_referer" ' 20 | '"$http_user_agent" "$http_x_forwarded_for"'; 21 | 22 | sendfile on; 23 | tcp_nopush on; 24 | tcp_nodelay on; 25 | 26 | keepalive_timeout 65; 27 | 28 | #gzip on; 29 | 30 | include /etc/nginx/conf.d/*.conf; 31 | } 32 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DEFAULT_EXPIRE_TIME="7d" 4 | DEFAULT_JPG_QUALITY="95" 5 | DEFAULT_MEMORY_SIZE="20m" 6 | DEFAULT_CACHE_NAME="cache_zone" 7 | DEFAULT_CACHE_SIZE="40m" 8 | DEFAULT_INACTIVE_TIME="30d" 9 | 10 | [ -z "$NGINX_HOST" ] && echo "missing NGINX_HOST variable" && exit 1 11 | [ -z "$IMAGE_HOST" ] && echo "missing IMAGE_HOST variable" && exit 1 12 | [ -n "$EXPIRE_TIME" ] && DEFAULT_EXPIRE_TIME=$EXPIRE_TIME 13 | [ -n "$JPG_QUALITY" ] && DEFAULT_JPG_QUALITY=$JPG_QUALITY 14 | [ -n "$MEMORY_SIZE" ] && DEFAULT_MEMORY_SIZE=$MEMORY_SIZE 15 | [ -n "$CACHE_NAME" ] && DEFAULT_CACHE_NAME=$CACHE_NAME 16 | [ -n "$CACHE_SIZE" ] && DEFAULT_CACHE_SIZE=$CACHE_SIZE 17 | [ -n "$INACTIVE_TIME" ] && DEFAULT_INACTIVE_TIME=$INACTIVE_TIME 18 | 19 | IMAGE_HOST=${IMAGE_HOST//\//\\/} 20 | sed -i -e "s/\${NGINX_HOST}/$NGINX_HOST/" /etc/nginx/conf.d/default.conf 21 | sed -i -e "s/\${IMAGE_HOST}/$IMAGE_HOST/" /etc/nginx/conf.d/default.conf 22 | sed -i -e "s/\${EXPIRE_TIME}/$DEFAULT_EXPIRE_TIME/" /etc/nginx/conf.d/default.conf 23 | sed -i -e "s/\${JPG_QUALITY}/$DEFAULT_JPG_QUALITY/" /etc/nginx/conf.d/default.conf 24 | sed -i -e "s/\${MEMORY_SIZE}/$DEFAULT_MEMORY_SIZE/" /etc/nginx/conf.d/default.conf 25 | sed -i -e "s/\${CACHE_NAME}/$DEFAULT_CACHE_NAME/" /etc/nginx/conf.d/default.conf 26 | sed -i -e "s/\${CACHE_SIZE}/$DEFAULT_CACHE_SIZE/" /etc/nginx/conf.d/default.conf 27 | sed -i -e "s/\${INACTIVE_TIME}/$DEFAULT_INACTIVE_TIME/" /etc/nginx/conf.d/default.conf 28 | 29 | nginx -g "daemon off;" 30 | --------------------------------------------------------------------------------