Reverse Proxy for the Homelab
Last week, I finally got around to set up an nginx reverse proxy for my home lab. Because it turned out a bit more involved than I had anticipated, I decided to share my notes.
This has been a long-term item on the todo list. The main motivation being that it is to hard to remember all the ports I have configured on various devices, and that password-managers tend to suggest multiple logins (when ports are the only thing separating the urls).
An added requirement was that I wanted to get this working at home but also when vpn-ing in via tailscale.
Nginx via Docker
The first step was to create an nginx container. I used the following docker compose:
# docker compose
services:
nginx:
image: nginx:latest
container_name: nginx
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./snippets:/etc/nginx/snippets
ports:
- 80:80
- 443:443
The nginx.conf
looks roughly like this
# nginx config
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name portainer.tiny.pnet;
include /etc/nginx/snippets/proxy-defaults.conf;
location / {
proxy_pass http://192.168.0.42:9000;
}
}
server {
listen 80;
server_name plex.tiny.pnet;
include /etc/nginx/snippets/proxy-defaults.conf;
location / {
proxy_pass http://192.168.0.42:32400;
}
}
server {
listen 80 default_server;
server_name _;
return 404;
}
}
In the example, I want to reach my main home server as tiny
. It runs on 192.168.0.42
and here I only created routes for the portainer and plex frontends. More can be added using the same pattern. Devices with other addresses and server names work the same, but you will need to adapt the dns settings below if you want to use a different naming scheme. I wanted all devices to end with *.pnet
.
The referenced snippet proxy-defaults.conf
applies decent default settings and saves us a few copy-paster (via this blog post).
# snippets/proxy-defaults.conf
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_redirect off;
proxy_http_version 1.1;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
So far so good.
The reverse proxy should be running but we have no real way to check it yet.
DNS via Pi-hole
The next step is to make sure that the reverse proxy can be discovered on the local network. Unfortunately, I did not find a way to do this natively using just the fritzbox router (AVM has removed the option to configre local DNS long ago).
The next best better option is to use a Pi-hole, as it comes with a DNS server — and all my mobile devices that lack native adblockers use it anyway.
If you run an OpenWRT router, the procedure should be the similar.
In my case, the Pi-hole is just another docker container, running on the same machine. Note that I am running its web interface on port 8080
to keep 80
free for nginx.
For completeness:
# docker compose
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
ports:
- "53:53/tcp"
- "53:53/udp"
- "8080:80/tcp"
dns:
- 127.0.0.1
- 8.8.8.8
volumes:
- '~/docker/pihole/etc-pihole:/etc/pihole'
- '~/docker/pihole/etc-dnsmasq.d:/etc/dnsmasq.d'
restart: unless-stopped
Now, Pi-hole has an option to add local DNS entries vai the GUI.
For Example, we could add portainer.tiny.pnet
and map it to 192.168.0.42:9000
. But then we have to add each device twice, once in Pi-hole and once in nginx.
To avoid the robotic work, I used a wildcard. While not possible via the GUI (see this reddit), all that is needed are two lines in a config. I created a new file in Pi-hole’s dnsmasq directory:
# /etc/dnsmasq.d/02-pnet.conf
address=/*.pnet/
address=/*.pnet/192.168.0.42
Restart Pi-hole after the config changes, and we are almost done.
What remains to do is to tell your devices to use Pi-hole as their DNS server. It does may or may not to be the primary one, the reverse proxy should work in either case. However, if a second DNS is configured, the ad-blocking won’t work. One could also set the Pi-hole as the primary (or secondary) DNS server in the fritzbox to expose it to all network devices.
To confirm that the DNS part is working, nslookup
should return something like this:
> nslookup portainer.tiny.pnet
Server: 100.100.100.100
Address: 100.100.100.100#53
Name: portainer.tiny.pnet
Address: 192.168.0.42
Split-DNS in Tailscale
In my case, using tailscale, the steps to get the reverse proxy also working when not at home were fairly simple.
- Login to the tailscale admin console and add a DNS entry
- Admin console > DNS > Add Nameserver > Custom
- Add Pi-hole ip
- Enable Split DNS
- Set domain to
pnet
Notes
- To reach my home-assistant container, I had to convince it to play nice with the reverse proxy. See here
Links
Climbing a Fountain
On September 19, I successfully defended my PhD thesis.
Now, a few weeks later, the realization of what this overwhelming day actually means has slowly sunk in. I am looking back at an incredible time in Viola’s group, and I am happy about this subtle reminder to stop every now and then, catch a breath, and appreciate all the good things that came my way.
I feel sentimental to see this chapter coming to an end, and about having to part ways with many people whom I have grown very fond of. I can’t thank you guys enough, for the countless discussions, emotional support, frequent advice, debugging sessions, or the occasional rant, all of which I enjoyed deeply.
What’s next?
Of course, as Göttingen tradition demands, after the defense I climbed the Gänseliesel fountain in the city center. It was a truly unique experience, and I am happy I could share it with so many of you, family, friends and colleagues, the lines between which have become increasingly blurry over the years.
While typing this post and putting up the photo, I got to see the metaphor in this peculiar tradition. Being up there gives you a good look at where you came from — but also a glimpse of where you are going next.
For me, after a few weeks of vacation and christmas with the family, I am planning to leave academia behind for something more applied: I am super excited to work (and code!) in a larger team, and to come up with well-designed solutions that help people in their everyday life.
Stimulating Cultures
As of last week, I have a new preprint on arXiv (edit: out now in Science Advances).
First of all, I want to thank the amazing team who worked on this project, especially Hideaki, Johannes and Jordi who had to endure grumpy me
during countless group meetings (and a bunch of extra ones). Seriously, thanks guys!
Brains are modular and cultures should be, too!
When presenting this in talks, I start by explaining that it would be cool to have neuronal cultures that well represent the living brain. However, as most of you probably know, cultures do their own thing and tend to be bursty — where occasional, rather short and synchronous events of high activity (bursts) take turns with extended episodes of silence. In 2018, Hideaki and his lab managed to limit these bursts (which usually light up the whole system) to sub-parts of the culture. They achieved this by making the topology of the cultures modular, effectively making it harder for a local burst to propagate to other modules. Thus, the effect was strongest when modules were at the brink of being disconnected from one another. Although individual modules still show the burst-like dynamics, the dynamics of the whole system are less synchronized, getting much closer to real-brain dynamics.
Brains are busy places.
Looking for another aspect that real brains have and cultures lack, we started to consider background noise. Think of it: Brains are constantly exposed to sensory stimuli, which tend to make their way into the dynamics one way or another, leading to an omnipresent (noisy) baseline activity of all neurons. On the other hand, cultures sit around in a glass dish, and although they perceive more of their environment than we usually expect, they do not have much to do. Hence, in this work we stimulated the cultures in a random and asynchronous manner. Adding such an asynchronous input reduced synchrony further than modularity alone.
We then used simulations of LIF-Neurons (using the awesome Brian2 simulator) and developed a minimal, mean-field like model to explain the reduced synchrony under stimulation. We found that the noisy input that makes neurons fire sporadically depletes the average synaptic resources in modules. This can best be seen when considering module-level trajectories in the Rate-Resource plane, as you can see on the right-hand side in the clip below. For long times, when no module-level burst occurs, resources recover and firing-rates are low. Once charged enough, a burst occurs and resources discharge rapidly as the modules’ neurons fire at high rate.
As you might guess from the clip, the noisy input does not only deplete the average amount of synaptic resources, it also lowers the minimum amount of resources needed to start module-level bursts (increasing their frequency). Now, due to the inhomogeneous degree distributions that are caused by the modular architecture (few axons connect across modules, but many axons connect within) the input-driven resource depletion affects the activations across modules more than within. Thus, module-level bursts persist but system-wide synchronization is reduced.
Links
- Our paper in Science Advances
- The Preprint on arXiv
- Yamamoto et al., Impact of modular organization on dynamical richness in cortical networks
- Zierenberg et al., Homeostatic Plasticity and External Input Shape Neural Network Dynamics
- Brian2, A clock-driven simulator for spiking neural networks
- Github has all my code for simulations, analysis, plotting, and movie rendering
Mr. Estimator
Mister Estimator is an open-source python toolbox I wrote as my first project in Viola’s group. It allows you to estimate the intrinsic timescale, e.g. in electrophysiologal data.
Originally intended for the analysis of time series from neuronal spiking activity, it works on a wide range of systems where subsampling is a problem (often, it is impossible to observe the whole system in full detail). Applications range from epidemic spreading to any system that can be represented by an autoregressive process.
Why care about this timescale? In general, it serves as a proxy for the distance to criticality and quantifies a system’s dynamic working point. And in the context of neuroscience, you can think of it as the duration over which any perturbation lingers within the network; it has been used as a key observable to uncover the functional hierarchy across primate cortex, and it serves as a measure of working memory.
See the repository on GitHub for more details.
About Color
I was looking for suitable color maps the other day when trying to squeeze too much data into a plot. Usually I prefer to just remove some details for the sake of clarity, but even then, color matters.
Procrastinating away the afternoon, I stumbled upon this super nice article on color palettes by Samantha Zhang. She gives a comprehensive overview of aspects to consider when picking colors, such as how to make your plots accessible to colorblind readers. Best of all, she provides a long list of resources, links and tools that help with the process.
Since then, Samantha’s article became my go-to resource on color in data science, and I am currently testing out three of her color maps in a paper draft. Below you find a python snippet to mimic them in matplotlib.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
custom_cmaps = dict()
custom_cmaps["cold"] = [
(0, "white"),
(0.25, "#DCEDC8"),
(0.45, "#42B3D5"),
(0.75, "#1A237E"),
(1, "black"),
]
custom_cmaps["hot"] = [
(0, "white"),
(0.3, "#FEEB65"),
(0.65, "#E4521B"),
(0.85, "#4D342F"),
(1, "black"),
]
custom_cmaps["pinks"] = [
(0, "white"),
(0.2, "#FFECB3"),
(0.45, "#E85285"),
(0.65, "#6A1B9A"),
(1, "black"),
]
def cmap_for_mpl(colors, n_bins=512):
return LinearSegmentedColormap.from_list("custom_cmap", colors, N=n_bins)
# for functions that use color map objects
cmap = cmap_for_mpl(custom_cmaps["pinks"])
# or to get discrete color values, call cmap() with a value between 0 and 1
num_lines = 5
for idx in range(num_lines):
clr = cmap((idx + 1) / (num_lines + 1))
x = np.arange(100)/np.pi
plt.axis('off')
plt.plot(x, np.sin(x + idx*np.pi/4) + idx, label=idx, color=clr)
Links
COVID-19 Inference and Forecast
How effective are interventions?
Our paper about estimating the effects of governmental interventions on the spread of COVID-19 is now out in Science!
Over the past months, my colleagues and I have worked on modeling the COVID-19 spread in Germany. Our approach uses Bayesian inference and Markov-Chain Monte-Carlo sampling on an SIR-model to find epidemiological parameters. It allows us to identify change points in the spreading rate (that is, when and how much the spreading rate changes). Check the links below for all the details!
I want to take the opportunity to thank everyone involved for this amazing collaboration. This has been, and still is, a truly great team effort. I feel that we have made a valuable contribution, and for me personally, the project made quarantine and working from home much more enjoyable! Thanks guys!
Website Content from Markdown
Time to celebrate, this is my first content that uses the new format.
For a while I have been noticing that it feels too cumbersome to add content to this site; writing is hard and it is harder when you do it in html. Then there is markdown, which feels completely opposite. It is convenient, easy to read and easy to write in any editor. You can even write a paper with colleagues in real time, without ever leaving your browser. Seriously, if you are not familiar with markdown yet, let me recommend spending a few minutes of procrastination to check it out.
Considering the goal to get markdown files onto my custom site, the only hurdle was to render the .md
files to html.
First, I considered parsing only once before uploading everything so that page loads remain snappy.
Then I realized that parsing an average document takes less than 10ms and ended up using Parsedown, a renderer in php.
This allows me to simply drop the .md
files into a folder on the server, php fetches them and parsedown creates the html for every file.
See this snippet of php
:
foreach (glob("folder_with_markdown_files/*.md") as $file) {
$html = Parsedown::instance()->text(file_get_contents($file));
echo "<hr><div class='markdown'>";
echo $html;
echo "</div>";
}
Related things to check out
- Parsedown, fast php markdown parser
- Marked 2, great markdown previewer on macOS
- CodiMD, real-time markdown editor, in the browser
- Markdown Cheatsheet
Criticality lies in the sampling.
For decades neuroscientists have argued that the cortex might operate at a critical point of a (second-order, non-equilibrium) phase transition. Operating at such a critical point would benefit neuronal networks because it enables optimal information-processing and useful quantities such as the correlation length are maximized.
The evidence that supports or contradicts this hypothesis of criticality in the brain often derives from measurements of neuronal avalanches. If a system is critical, the probability distributions of the size (and duration) of these avalanches follow a power law. Thus, power-law distributions are a common way to check if a system is critical.
Controversially, the results of studies that build on observing power-laws in the neuronal avalanches vary immensely throughout the literature; some (and I am skipping many details) find the brain in a critical state and others in a subcritical state.
We found that the cause of the controversy lies in the way the avalanches are sampled. If an electrode’s signal is used directly (e.g. LFP), then many neurons contribute to the signal, and the events that make up an avalanche have many contributions. Because of the many contributions, spurious correlations are introduced, and this type of coarse-sampling can produce power-law distributions — even when the observed system is not critical.
AtmoCL and AtmoWEB
AtmoCL is an OpenCL port of the All Scale Atmospheric Model (ASAM) that I worked on during my time at Tropos. The code was initially based on the OpenGL derivative ASAMgpu. While OpenGL as a base for the initial GPU model was the intuitive choice, the (back then) more recent OpenCL offered some neat advantages. Apart from allowing the same code to run on a variety of hosts including heterogeneous environments of GPUs, CPUs and accelerators, we could profit from the 3D image class. The mapping from 3D volume to 2D textures - which are the favourable memory format for GPUs - is done by the driver. Further, one can directly access any point of the volume through integer indices instead of the more cumbersome float coordinates, inherent to OpenGL.
One main idea was to export the model state as images where the volume is mapped to 2D cutplanes and state variables are presented in RGB. To animate the pictures as a moving sequence, I developed a lightweight webinterface using bootstrap. It also plots vertical profiles and time series with highcharts. Checkout the demo.