{"id":35159910,"url":"https://github.com/sbarre/nginx-reverse-proxy-demos","last_synced_at":"2026-04-28T13:01:21.278Z","repository":{"id":95879212,"uuid":"303812425","full_name":"sbarre/nginx-reverse-proxy-demos","owner":"sbarre","description":"Some simple nginx demos to show off the various ways you can proxy a dynamic web application.","archived":false,"fork":false,"pushed_at":"2024-02-06T02:06:52.000Z","size":3913,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-02-06T13:40:52.278Z","etag":null,"topics":["nginx","reverse-proxy"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sbarre.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2020-10-13T19:46:24.000Z","updated_at":"2023-08-04T13:43:05.000Z","dependencies_parsed_at":"2023-03-14T19:45:24.198Z","dependency_job_id":null,"html_url":"https://github.com/sbarre/nginx-reverse-proxy-demos","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/sbarre/nginx-reverse-proxy-demos","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sbarre%2Fnginx-reverse-proxy-demos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sbarre%2Fnginx-reverse-proxy-demos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sbarre%2Fnginx-reverse-proxy-demos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sbarre%2Fnginx-reverse-proxy-demos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sbarre","download_url":"https://codeload.github.com/sbarre/nginx-reverse-proxy-demos/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sbarre%2Fnginx-reverse-proxy-demos/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32381696,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-28T11:25:28.583Z","status":"ssl_error","status_checked_at":"2026-04-28T11:25:05.435Z","response_time":56,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nginx","reverse-proxy"],"created_at":"2025-12-28T17:59:48.175Z","updated_at":"2026-04-28T13:01:21.272Z","avatar_url":"https://github.com/sbarre.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Nginx demos showing off various proxy strategies\n\nThis repository sets up a basic Fastify Node app, and provides a set of nginx docker containers that proxy this Node application in order to demonstrate a variety of nginx features.\n\n- [Reverse Proxy](#reverse-proxy-port-8080)\n- [Static Serving](#static-serving-port-8081)\n- [Full Cache](#full-cache-port-8082)\n- [Error Handling](#error-handling-port-8083)\n- [Nginx JS Module](#nginx-js-module-port-8084)\n- [Nginx Split Clients Module](#nginx-split-clients-module-port-8085)\n- [Nginx Cache On Error](#nginx-cache-on-error-port-8086)\n\n### Initial Setup for all demos\n\n1. You need to have Node (v12+) and Docker installed on your machine.\n2. From the `app-server` folder, run `npm install` to download the Node app dependencies.\n\n## Reverse Proxy (Port 8080)\n\nIn this configuration, nginx acts as a reverse proxy to a horizontally-scaled web application with 3 instances.\n\nStart 3 copies of the node app by switching to the **app-server** folder and running `npm run start3`. This will run the app on ports:\n\n- [http://localhost:3000](http://localhost:3000)\n- [http://localhost:3001](http://localhost:3001)\n- [http://localhost:3002](http://localhost:3002)\n\nIn a different terminal session, from the root of the project, run `docker-compose up` to bring up the containers.\n\nTo stop both apps, you can press CTRL-C in the appropriate terminal.\n\nVisit [http://localhost:8080/](http://localhost:8080) to see the load-balanced application being proxied. You will notice the page title is **\"Homepage 300x\"** where the \"x\" is either 0, 1 or 2. Refresh the page a few times and you should see the number changing. This indicates which node application nginx is sending the request to each time you load the page.\n\nNginx is using a default configuration like this which weighs each server equally. This is called a **Round Robin** strategy:\n\n```nginx\nupstream nodeapp {\n  server host.docker.internal:3000;\n  server host.docker.internal:3001;\n  server host.docker.internal:3002;\n}\n```\n\nIt is possible to add a `weight` propery to each server to bias the number of requests, which would look like this:\n\n```nginx\nupstream nodeapp {\n  server host.docker.internal:3000 weight=5;\n  server host.docker.internal:3001 weight=3;\n  server host.docker.internal:3002;\n}\n```\n\nThis would send five times as many requests to the first server and three times as many requests to the second server as to the third one.\n\nIn addition to Round Robin, it is also possible to apply different specific strategies based on your use-case. The [nginx documentation](https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#choosing-a-load-balancing-method) covers these, but in summary they are:\n\n2. **Least Connections**: Requests are sent to the server with the least active connections. Server weights still apply.\n\n```nginx\nupstream nodeapp {\n  least_conn;\n  server host.docker.internal:3000;\n  server host.docker.internal:3001;\n  server host.docker.internal:3002;\n}\n```\n\n3. **IP Hash**: Requests are sent to a given server based on the client's IP address, so all traffic from a given IP will go to the same server (as long as the server is available).\n\n```nginx\nupstream nodeapp {\n  ip_hash;\n  server host.docker.internal:3000;\n  server host.docker.internal:3001;\n  server host.docker.internal:3002;\n}\n```\n\n4. **Request Hash**: Requests are sent to a given server based on a hash constructed from the supplied variables.\n\n```nginx\nupstream nodeapp {\n  hash $request_uri;\n  server host.docker.internal:3000;\n  server host.docker.internal:3001;\n  server host.docker.internal:3002;\n}\n```\n\n_Warning:_ The two Hash methods above may sound useful and simple but they have a lot of hidden complexities and are not recommended. If you are trying to implement [sticky sessions](https://dev.to/gkoniaris/why-you-should-never-use-sticky-sessions-2pkj) you are better off doing it on the application side with a shared session cache (using Redis for example) instead of relying on nginx to route users to the same server for an entire session.\n\n5. **Random**: Requests are sent to a random server.\n\n```nginx\nupstream nodeapp {\n  random;\n  server host.docker.internal:3000;\n  server host.docker.internal:3001;\n  server host.docker.internal:3002;\n}\n```\n\nThere are many other directives you can specify in an upstream block to tailor the behaviour to your use-case, the full list is [here](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream).\n\n## Static Serving (Port 8081)\n\nIn this configuration, nginx acts as a reverse proxy to a dynamic web application but directly serves all the static files.\n\nStart the node app by switching to the **app-server** folder and running `npm run start`. This will run the app on port [3000](http://localhost:3000).\n\nIn a different terminal session, from the root of the project, run `docker-compose up` to bring up the containers.\n\nVisit [http://localhost:8081/](http://localhost:8081) to see the application being proxied with static asset serving.\n\nTo stop both apps, you can press CTRL-C in the appropriate terminal.\n\nThe benefit provided by static serving is to reduce the resource usage of your node application by allowing it to only serve dynamic requests, and to let nginx handle serving things like HTML, CSS, Javascript files and other static assets like images, documents, fonts, etc...\n\nWe set this up in nginx using this configuration:\n\n```nginx\nlocation /public {\n  root /var/www/nginx/html;\n}\n```\n\n#### Limitations\n\nFor this to work, nginx needs to have filesystem access to the static assets of your dynamic application. If both nginx and your app are running on the same server, this is straightforward. However, if your application is not running on the same server, you may need to mount the filesystem across the network, which is not always possible, or desirable.\n\nIn this demo we are mounting the static file path as a read-only volume into the nginx Docker container:\n\n```\nvolumes:\n  - ./app-server/public:/var/www/nginx/html/public:ro\n```\n\nTo see this feature in action, visit the proxy URL at [http://localhost:8081/](http://localhost:8081) and check in your terminal sessions that are running Docker and your Node app. In your Docker window, you will see log lines for all the page assets, including the CSS, Javascript and images. In the Node window, you should only see 1 request for the `/` route, and it should not be sending any static files.\n\n#### Direct responses\n\nAnother feature related to static serving that is implemented in this configuration is the ability to have nginx return content directly from the configuration.\n\nIn this first example we are configuring a location block to handle `/robots.txt` requests. We are returning a simple string as the content for the request, and there is no actual **robots.txt** file on the disk. We are also turning off the logging of these requests.\n\n```nginx\nlocation = /robots.txt {\n  access_log off;\n  return 200 \"User-agent: *\\r\\nDisallow: *\";\n}\n```\n\nAnother popular pattern is to use nginx to handle requests for a favicon. Many clients make these requests automatically, and this can add a lot of noise to your error logs if you don't have one. So we use a special nginx directive to return an empty image for these requests (and again we disable logging of these requests):\n\n```nginx\nlocation = /favicon.ico {\n  access_log off;\n  empty_gif;\n}\n```\n\nIn the final example, we create a very basic API-like response that returns a JSON block with the client's IP address and a timestamp. Something like this could be used for a simple health-check endpoint:\n\n```nginx\nlocation = /my-ip {\n  add_header Content-Type application/json;\n  return 200 \"{ \\\"ip\\\": \\\"$remote_addr\\\", \\\"timestamp\\\": \\\"$time_local\\\" }\\r\\n\";\n}\n```\n\n## Full Cache (Port 8082)\n\nIn this configuration, nginx acts as a reverse proxy to a dynamic web application and caches the (almost) complete output of the app on a specified time interval.\n\nStart the node app by switching to the **app-server** folder and running `npm run start`. This will run the app on port [3000](http://localhost:3000).\n\nIn a different terminal session, from the root of the project, run `docker-compose up` to bring up the containers.\n\nVisit [http://localhost:8082/](http://localhost:8082) to see the application being proxied with full caching.\n\nTo stop both apps, you can press CTRL-C in the appropriate terminal.\n\nTo set up the full proxy cache, you must start by adding a cache directive to your main nginx configuration:\n\n```nginx\nproxy_cache_path /var/tmp/cache max_size=20m keys_zone=appcache:10m;\n```\n\nIn the directive above, we are setting the path to the cache at `/var/tmp/cache` on the server that nginx is running on, _not_ the server being proxied. Unlike the static serving strategy in the previous demo, you can use nginx's caching strategy even when nginx and your app server are on different machines.\n\nWe then set the `max_size=20m` directive which tells nginx to use a maximum of 20 megabytes for caching. This directive is optional but it is a good practice to set anyways. You will need to adjust this value to your specific scenario but remember that this strategy will cache _all_ requests coming from th configured locations, including images, videos and other assets. This may or may not be what you want to do.\n\nThe next directive `keys_zone:appcache:10m` defines a named cache zone and its size. We will reference this name in our `server` block later.\n\nOnce we have this initial setup, we declare our caching parameters in our server block:\n\n```nginx\nproxy_cache appcache;\nproxy_cache_methods GET;\nproxy_cache_valid 200 1m;\nproxy_cache_valid 404 10m;\n```\n\nWe first reference our cache zone `appcache` that we defined earlier, and we declare which request methods we want to cahed (in this case we only cache GET calls). We then further declare that we only want to cache responses that have 200 or 404 status codes, and then for how long we want to cache them (1 minute and 10 minutes respectively).\n\nThis declares the overall caching strategy for all locations within this server block. However, we can adjust or override this behaviour on per-server block:\n\n```nginx\nlocation = /favicon.ico {\n  access_log off;\n  proxy_no_cache 1;\n  empty_gif;\n}\n\nlocation /public {\n  proxy_cache_valid 200 60m;\n}\n```\n\nUsing the `proxy_no_cache` directive we tell nginx not to cache the `/favicon.ico` location because we are generating that one ourselves and returning an empty GIF directly from nginx, and we are telling nginx to cache the `/public` path which serves all the static assets (JS, CSS, images) for 60 minutes at at time, since these assets are unlikely to change.\n\n#### Further considerations\n\nWhile this approach can have noticeable performance benefits for your application, you must consider the impact it can have on dynamic pages, particularly if you are doing per-user customization. Nginx caches globally _for all users_ and not per-user.\n\nThe [proxy_cache_key](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_key) directive gives you more control over how nginx determines if a given request has been cached, and you can incorporate elements such as a user's cookie string into the key to do per-user caching (but then think about the size of your cache depending on how many users you have).\n\nA more [extensive guide to caching](https://www.nginx.com/blog/nginx-caching-guide/) is also available on the nginx website, along with the [proxy module docs](http://nginx.org/en/docs/http/ngx_http_proxy_module.html) that cover all the possible options and settings for proxy caching.\n\n## Error Handling (Port 8083)\n\nIn this configuration, we demonstrate two different approaches that nginx uses to handle errors coming from a proxied application.\n\nStart the node app by switching to the **app-server** folder and running `npm run start`. This will run the app on port [3000](http://localhost:3000).\n\nIn a different terminal session, from the root of the project, run `docker-compose up` to bring up the containers.\n\nVisit [http://localhost:8083/](http://localhost:8083) to see the application being proxied.\n\nTo stop both apps, you can press CTRL-C in the appropriate terminal.\n\n## Nginx JS Module (Port 8084)\n\nIn this configuration, we demonstrate the nginx Javascript module that can be used to do more complex logic on your requests.\n\nStart the node app by switching to the **app-server** folder and running `npm run start`. This will run the app on port [3000](http://localhost:3000).\n\nIn a different terminal session, from the root of the project, run `docker-compose up` to bring up the containers.\n\nVisit [http://localhost:8084/](http://localhost:8084) to see the application homepage.\n\nVisit [http://localhost:8084/uppercase](http://localhost:8084/uppercase) to see the application homepage after being run through an upper-case filter.\n\nIn this example we must replace the whole nginx configuration rather than just the default include, because we have to load the nginx module at the top of the file in the global scope:\n\n```nginx\nload_module /usr/lib/nginx/modules/ngx_http_js_module.so;\n```\n\nThen in our `http` scope we have to import our scripts file:\n\n```nginx\njs_import scripts/http.js;\n```\n\nAnd finally inside our `server` block, we have the following two location blocks:\n\n```nginx\nlocation / {\n  proxy_set_header X-Real-IP $remote_addr;\n  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n  proxy_set_header X-Nginx-Proxy true;\n  proxy_pass http://host.docker.internal:3000;\n  proxy_set_header Host $http_host;\n  proxy_cache_bypass $http_upgrade;\n  proxy_redirect off;\n}\n\nlocation ~ /uppercase {\n  js_content http.uppercase;\n}\n```\n\nThe first block is simply creating a proxy to our underlying node application. The second block creates a `/uppercase` location that will call our Javascript function that proxies the main location and returns a response that uppercases parts of the page in real-time.\n\nLook at the `js-module/scripts/http.js` file to see the script that does the request manipulation. It's pretty basic but it demonstrates how you make a subrequest and manipulate it on the fly before returning it to the client.\n\nTo stop both apps, you can press CTRL-C in the appropriate terminal.\n\n#### Further reading\n\nNginx Javascript is quite powerful but not a particularly popular feature, and so it can be hard to find out how to use it well due to the lack of examples available in the community.\n\nThe [official documentation](https://nginx.org/en/docs/njs/) provides a good introduction (the References and Examples section will be of most use).\n\nBe sure to also review the [compatibility guide](http://nginx.org/en/docs/njs/compatibility.html) to understand the subset of Javascript that is available to you.\n\nThe [njs-examples](https://github.com/xeioex/njs-examples) Github repo has additional examples of NJS usage.\n\n## Nginx Split Clients Module (Port 8085)\n\nIn this configuration, we demonstrate the nginx Split Clients module (with some help from the JS module) that can be used to do more complex logic on your requests.\n\nStart the node app by switching to the **app-server** folder and running `npm run start3`. This will run 3 copies of the the Node app on ports 3000, 3001 and 3002 like our first demo.\n\nIn a different terminal session, from the root of the project, run `docker-compose up` to bring up the containers.\n\nVisit [http://localhost:8085/](http://localhost:8085) to see the application homepage.\n\n## Nginx Cache On Error (Port 8086)\n\nServe a cached version when the backend is down.\n\n## Nginx with Lua (Port 8087)\n\nUse LuaJIT as a scripting language to extend nginx.\n\n**IMPORTANT:** This image is commented out of the `docker-compose.yml` file by default.\n\nYou have to build this image separately first.\n\n```\n$ cd lua\n$ docker build --build-arg ENABLED_MODULES=\"ndk lua\" -t nginx-lua .\n```\n\nThis will build a local image based on `nginx:mainline-alpine` and add in the Lua module. After you have successfully built the image, you can un-comment the lines in `docker-compose.yml` to include this image.\n\n### Why do we do this?\n\nThis might be my fault? For some reason doing an inline build with `docker-compose up` or `docker-compose build` fails because docker complains about needing BuildKit enabled (which should be enabled by default in Docker v23+). If anyone knows how to solve this, please open an issue!\n\nThe configuration ideally would build and save the image automatically:\n\n```\n  nginx_lua:\n    container_name: nginx_lua\n    ports:\n      - \"8087:8080\"\n    build:\n      context: ./lua\n      args:\n        ENABLED_MODULES: ndk lua\n    volumes:\n      - ./lua/nginx.conf:/etc/nginx/nginx.conf:ro\n      - ./lua/scripts:/etc/nginx/scripts:ro\n```\n\n## Upstream Backup (Port 8088)\n\nThis configuration sets 2 servers in the `upstream{}` configuration, one on port 3000 and one on port 3001, but when you visit `http://localhost:8088` you will notice that you only hit the server on port 3000. Port 3001 will only be served if the port 3000 server is detected as being down.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsbarre%2Fnginx-reverse-proxy-demos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsbarre%2Fnginx-reverse-proxy-demos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsbarre%2Fnginx-reverse-proxy-demos/lists"}