Showing posts with label nginx. Show all posts
Showing posts with label nginx. Show all posts

Sunday, 26 April 2026

Home-lab design (2026 edition) part 3 - Inside the Docker VM

This is the next part of my progressively-deeper look at my "home lab" setup in 2026 - now we're peering into the Docker VM itself. There are a million "here are the 19 Docker apps you ABSOLUTELY MUST run in 2026" listicles out there, that's fine. I don't run that many, and many of them are of a highly-personalised, only-on-my-network-thankyou nature that I'm not even going to mention. The key two (yes, just two) are [Nginx Proxy Manager](https://nginxproxymanager.com/) and [Arcane](https://getarcane.app/). The first because it means I can forget about individual internal network addresses, port numbers, and "this website is not secure" non-HTTPS browser warnings, the latter because it's a dead simple dashboard to see what's going on at the Docker level. I'm not even going to talk any more about Arcane because it's just so simple; it's exactly what you'd expect a Docker web UI to look like:
## Nginx Proxy Manager ALL THE THINGS NPM is just a really nice and simple frontend for an Nginx instance, and I must admit I may have got a little carried away re-front-ending almost every local-network-based web app I regularly use, viz: - Arcane - HAOS - My Synology NAS - My Mikrotik router - Octoprint - My Raspberry Pi - The Proxmox UI - My DLNA streamer, Serviio - My ISP router (a TP-Link) - All my personal scratch-my-own-itch apps; and - Nginx Proxy Manager itself(!) ### Setting up Now *most* of these webapps and Dockerized apps work *great* with Nginx Proxy Manager providing a wildcard HTTPS certificate and proxying for them. If it's a simple app like `http://nas:5000` it's the work of seconds to give it a better address. For a new Dockerized webapp, my process is: - Get it going and find out what port it's on, i.e. `webport` - Check I can hit it at `http://docker:webport` - Go into NPM and add a proxy host for `coolapp.lab.themillhousegroup.com` that points to `http://docker:webport`, and add force SSL on the SSL tab using my `*.lab.themillhousegroup.com` wildcard certificate - I've already got a corresponding "wildcard" DNS `CNAME` entry in the Mikrotik router that ensures `*.lab.themillhousegroup.com` will resolve to `docker`:
NPM automagically manages this certificate for me thanks to the wonders of the `DnsMulti` DNS-based certificate management system, which does the whole Lets Encrypt automated-renewal dance via any one of a [huge number of DNS providers](https://go-acme.github.io/lego/dns/#dns-providers) (I use Netlify which is as simple as configuring a Personal Use Token and giving it to the `dnsmulti` plugin as an environment variable) There are a couple of "difficult" apps that needed a little more configuration though. ### Home Assistant Doesn't take kindly to being proxied by NPM and has to have a couple of (very simple) stanzas added to its `configuration.yaml`: ``` # Allow NGinx Proxy Manager to proxy for homeassistant: # https://community.home-assistant.io/t/using-nginx-proxy-manager-with-homeassistant-all-via-docker/725019/6 # https://www.reddit.com/r/homeassistant/comments/1dp4nwf/hass_behind_nginx_proxy_manager/ http: use_x_forwarded_for: true trusted_proxies: - 10.240.0.105 # IP of docker - 10.240.0.0/24 # the Docker subnet - 127.0.0.1 # Add the localhost IPv4 address - ::1 # Add the localhost IPv6 address # ... and further to that: # https://community.home-assistant.io/t/using-nginx-proxy-manager-with-homeassistant-all-via-docker/725019/13 homeassistant: external_url: "https://haos.lab.themillhousegroup.com" ``` As you can probably tell, I like pasting the relevant web link into configuration docs; those links are to various fora - [Reddit `/r/homeassistant`](https://www.reddit.com/r/homeassistant/comments/1dp4nwf/hass_behind_nginx_proxy_manager/) for the `trusted_proxies` IP addresses and the [Home Assistant Community](https://community.home-assistant.io/t/using-nginx-proxy-manager-with-homeassistant-all-via-docker/725019/13) for the `external_url` setting. ### Proxmox Proxmox *really* wants you to use HTTPS everywhere, so you need to turn on ALL the SSL options in NPM, and ALSO use https *in the address of the proxied instance as well* - which is unusual, but makes sense, as it does create a self-signed certificate when you first set up your system. I basically followed [this guide on Reddit](https://www.reddit.com/r/Proxmox/comments/1jsgto6/accessing_proxmox_via_nginx_proxy_manager/) so my NPM setup for `proxmox.lab.themillhousegroup.com` looks like this (note the `https` in the Scheme - different to every other host!):

Thursday, 23 May 2013

nginx as a CORS-enabled HTTPS proxy

So you need a CORS frontend to your HTTPS target server that is completely unaware of CORS.

I tried doing this with Apache but it couldn't support the creation of a response to the "preflight" HTTP OPTIONS request that is made by CORS-compliant frameworks like jQuery.

Nginx turned out to be just what I needed, and furthermore it felt better too - none of that module-enabling stuff required, plus the configuration file feels more programmer-friendly with indenting, curly-braces for scope and if statements allowing a certain feeling of control flow.

So without any further ado (on your Debian/Ubuntu box, natch) :

Get Nginx:
sudo apt-get install nginx

Get the Nginx HttpHeadersMore module which allows the CORS headers to be applied whether the request was successful or not (important!)
sudo apt-get install nginx-extras

Now for the all-important config (in /etc/nginx/sites-available/default ) - We'll go through the details after this:

#
# Act as a CORS proxy for the given HTTPS server(s)
#
server {
  listen 443 default_server ssl;
  server_name localhost;

  # Fake certs - fine for development purposes :-)
  ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
  ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

  ssl_session_timeout 5m;

  # Make sure you specify all the methods and Headers 
  # you send with any request!
  more_set_headers 'Access-Control-Allow-Origin: *';
  more_set_headers 'Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE';
  more_set_headers 'Access-Control-Allow-Credentials: true';
  more_set_headers 'Access-Control-Allow-Headers: Origin,Content-Type,Accept';

  location /server1/  {
    include sites-available/cors-options.conf;
    proxy_pass https://<actual server1 url>/;
  }

  location /server2/  {
    include sites-available/cors-options.conf;
    proxy_pass https://<actual server2 url>/;
  }
}

And alongside it, in /etc/nginx/sites-available/cors-options.conf:
    # Handle a CORS preflight OPTIONS request 
    # without passing it through to the proxied server 
    if ($request_method = OPTIONS ) {
      add_header Content-Length 0;
      add_header Content-Type text/plain;
      return 204;
    }
What I like about the Nginx config file format is how it almost feels like a (primitive, low-level, but powerful) controller definition in a typical web MVC framework. We start with some "globals" to indicate we are using SSL. Note we are only listening on port 443 so you can have some other server running on port 80. Then we specify the standard CORS headers, which will be applied to EVERY request, whether handled locally or proxied through to the target server, and even if the proxied request results in a 404:
  more_set_headers 'Access-Control-Allow-Origin: *';
  more_set_headers 'Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE';
  more_set_headers 'Access-Control-Allow-Credentials: true';
  more_set_headers 'Access-Control-Allow-Headers: Origin,Content-Type,Accept';

This last point can be important - your JavaScript client might need to inspect the body of an error response to work out what to do next - but if it doesn't have the CORS headers applied, the client is not actually permitted to see it!
The little if statement that is included for each location provides functionality that I simply couldn't find in Apache. This is the explicit response to a preflighted OPTIONS:
  
    if ($request_method = OPTIONS ) {
      add_header Content-Length 0;
      add_header Content-Type text/plain;
      return 204;
    }
The target server remains blissfully unaware of the OPTIONS request, so you can safely firewall them out from this point onwards if you want. A 204 No Content is the technically-correct response code for a 200 OK with no body ("content") if you were wondering.
The last part(s) can be repeated for as many target servers as you want, and you can use whatever naming scheme you like:
 location /server1/  {
    include sites-available/cors-options.conf;
    proxy_pass https://server1.example.com;
  }

This translates requests for:
    https://my.devbox.example.com/server1/some/api/url
to:
    https://server1.example.com/some/api/url

This config has proven useful for running some Jasmine-based Javascript integration tests - hopefully it'll be useful for someone else out there too.