<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Steffen Haase</title>
    <description>The latest articles on DEV Community by Steffen Haase (@steffen_haase_f0b7bbb25e0).</description>
    <link>https://dev.to/steffen_haase_f0b7bbb25e0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4016101%2F62da01b6-3330-4590-9459-73e4276f7326.jpg</url>
      <title>DEV Community: Steffen Haase</title>
      <link>https://dev.to/steffen_haase_f0b7bbb25e0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/steffen_haase_f0b7bbb25e0"/>
    <language>en</language>
    <item>
      <title>Securely Access Your Raspberry Pi Web Interfaces with an SSH Tunnel and Chromium/Chromium (or Firefox)</title>
      <dc:creator>Steffen Haase</dc:creator>
      <pubDate>Sun, 02 Aug 2026 03:07:34 +0000</pubDate>
      <link>https://dev.to/steffen_haase_f0b7bbb25e0/securely-access-your-raspberry-pi-web-interfaces-with-an-ssh-tunnel-and-chromiumchromium-or-3172</link>
      <guid>https://dev.to/steffen_haase_f0b7bbb25e0/securely-access-your-raspberry-pi-web-interfaces-with-an-ssh-tunnel-and-chromiumchromium-or-3172</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0a8tb8jonnwmllgdvca4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0a8tb8jonnwmllgdvca4.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you run services such as Pi-hole, Fail2Ban, Home Assistant, or other web applications on a Raspberry Pi, you've probably faced the same question:&lt;/p&gt;

&lt;p&gt;How can I access the web interface securely without exposing it to my local network or the Internet?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important note: This article is for Linux users, not for Windows user.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One elegant solution is SSH port forwarding.&lt;br&gt;
In this article, I'll show how I use a small Bash script that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creates an SSH tunnel to my Raspberry Pi&lt;/li&gt;
&lt;li&gt;forwards a local port to the remote web service&lt;/li&gt;
&lt;li&gt;automatically opens Chromium&lt;/li&gt;
&lt;li&gt;cleans everything up when I'm done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No reverse proxy. No VPN. No open ports.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why SSH Port Forwarding?
&lt;/h2&gt;

&lt;p&gt;Imagine your Raspberry Pi runs a web application that only listens on localhost: &lt;code&gt;127.0.0.1:8080&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That means the service is not reachable from the network, which is exactly what we want from a security perspective.&lt;/p&gt;

&lt;p&gt;SSH allows us to temporarily expose that service only to our own computer.&lt;/p&gt;

&lt;p&gt;The tunnel looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
    │
    │ http://127.0.0.1:8080
    │
Local PC
    │
SSH Tunnel
    │
Raspberry Pi
    │
127.0.0.1:8080
    │
Web Application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser thinks it's talking to a local web server, while SSH transparently forwards all traffic to the Raspberry Pi.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SSH&amp;nbsp;Command
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In this article I will use the IP address 192.168.0.10 for the Raspberry Pi server.&lt;/p&gt;

&lt;p&gt;The core of the solution is simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-L&lt;/span&gt; 8080:127.0.0.1:8080 &lt;span class="se"&gt;\&lt;/span&gt;
    my-user@192.168.0.10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The options mean:&lt;br&gt;
&lt;code&gt;-N&lt;/code&gt; – don't execute a remote shell&lt;br&gt;
&lt;code&gt;-L&lt;/code&gt; – create a local port forwarding&lt;br&gt;
&lt;code&gt;8080&lt;/code&gt; – local port&lt;br&gt;
&lt;code&gt;127.0.0.1:8080&lt;/code&gt; – destination on the Raspberry Pi&lt;/p&gt;

&lt;p&gt;After running the command, opening &lt;code&gt;http://127.0.0.1:8080/&lt;/code&gt;&lt;br&gt;
on your own computer actually connects to the service running on the Raspberry Pi.&lt;/p&gt;
&lt;h2&gt;
  
  
  Automating Everything
&lt;/h2&gt;

&lt;p&gt;Typing the SSH command every time gets old quickly, the Bash script automates the complete workflow.&lt;/p&gt;

&lt;p&gt;It performs the following steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Removes any stale tunnel from a previous run&lt;/li&gt;
&lt;li&gt;Creates a new SSH tunnel&lt;/li&gt;
&lt;li&gt;Verifies that the tunnel was established successfully&lt;/li&gt;
&lt;li&gt;Launches Chromium automatically&lt;/li&gt;
&lt;li&gt;Opens the desired web interface&lt;/li&gt;
&lt;li&gt;Cleans up the SSH tunnel when Chromium exits (or when the script is interrupted)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is essentially a one-click launcher.&lt;/p&gt;
&lt;h2&gt;
  
  
  Full shell&amp;nbsp;script
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="c"&gt;# fail2ban-ui SSH tunnel launcher&lt;/span&gt;

&lt;span class="c"&gt;# ===== CONFIG =====&lt;/span&gt;
&lt;span class="nv"&gt;PI_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"my-user"&lt;/span&gt;
&lt;span class="nv"&gt;PI_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.0.10"&lt;/span&gt;

&lt;span class="nv"&gt;LOCAL_HTTP_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"8080"&lt;/span&gt;
&lt;span class="nv"&gt;REMOTE_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;
&lt;span class="nv"&gt;REMOTE_HTTP_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"8080"&lt;/span&gt;

&lt;span class="c"&gt;# Browser binary name&lt;/span&gt;
&lt;span class="c"&gt;# The name of the browser binary. This is used to start the web browser,&lt;/span&gt;
&lt;span class="c"&gt;# and also to lookup the web browser process&lt;/span&gt;
&lt;span class="nv"&gt;CHROMIUM_BIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"chromium"&lt;/span&gt;

&lt;span class="nv"&gt;PID_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/fail2ban-ui-ssh.pid"&lt;/span&gt;
&lt;span class="c"&gt;# ==================&lt;/span&gt;

&lt;span class="c"&gt;# Pi-hole admin URL&lt;/span&gt;
&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOCAL_HTTP_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/admin/"&lt;/span&gt;

&lt;span class="c"&gt;# Cleanup function&lt;/span&gt;
cleanup&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] Cleaning up ..."&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PID_FILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nv"&gt;SSH_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PID_FILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;ps &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_PID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
            &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] Closing SSH tunnel ..."&lt;/span&gt;
            &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_PID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null
        &lt;span class="k"&gt;fi

        &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PID_FILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi

    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] Done"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;trap &lt;/span&gt;cleanup EXIT INT TERM

&lt;span class="c"&gt;# Removing any existing old SSH tunnel&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] Removing old tunnel (if existing) ..."&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PID_FILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;OLD_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PID_FILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;ps &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OLD_PID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OLD_PID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null
    &lt;span class="k"&gt;fi

    &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PID_FILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Create new SSH tunnel&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] Starting SSH tunnel ..."&lt;/span&gt;

ssh &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-p&lt;/span&gt; 22 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOCAL_HTTP_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REMOTE_HOST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REMOTE_HTTP_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PI_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PI_HOST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &amp;amp;

&lt;span class="nv"&gt;SSH_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_PID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PID_FILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;sleep &lt;/span&gt;2

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; ps &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_PID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[!] Failed to establish SSH tunnel"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] SSH tunnel established"&lt;/span&gt;

&lt;span class="c"&gt;# Chromium handling&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;pgrep &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CHROMIUM_BIN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] Chromium already running"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] Opening URL in new tab ..."&lt;/span&gt;

    &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CHROMIUM_BIN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] Press CTRL+C to close the SSH tunnel"&lt;/span&gt;

    &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_PID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] Chromium not running"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] Starting Chromium ..."&lt;/span&gt;

    &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CHROMIUM_BIN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--new-window&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;

    &lt;span class="nv"&gt;CHROMIUM_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[+] Waiting for Chromium to close ..."&lt;/span&gt;

    &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CHROMIUM_PID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;Only a few variables need to be adjusted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PI_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"my-user"&lt;/span&gt;
&lt;span class="nv"&gt;PI_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.0.10"&lt;/span&gt;
&lt;span class="nv"&gt;LOCAL_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"8080"&lt;/span&gt;
&lt;span class="nv"&gt;REMOTE_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;
&lt;span class="nv"&gt;REMOTE_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"8080"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explanations:&lt;br&gt;
&lt;code&gt;PI_USER&lt;/code&gt; - SSH username&lt;br&gt;
&lt;code&gt;PI_HOST&lt;/code&gt; - Raspberry Pi hostname or IP&lt;br&gt;
&lt;code&gt;LOCAL_PORT&lt;/code&gt; - Port exposed on your computer&lt;br&gt;
&lt;code&gt;REMOTE_HOST&lt;/code&gt; - Usually 127.0.0.1&amp;nbsp;&lt;br&gt;
&lt;code&gt;REMOTE_PORT&lt;/code&gt; - Port of the web application on the Pi&lt;/p&gt;
&lt;h2&gt;
  
  
  Automatic Cleanup
&lt;/h2&gt;

&lt;p&gt;One feature I particularly like is the cleanup function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;trap &lt;/span&gt;cleanup EXIT INT TERM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever the script exits, whether normally or because you press &lt;code&gt;Ctrl+C&lt;/code&gt;, it automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stops the SSH tunnel&lt;/li&gt;
&lt;li&gt;removes the PID file&lt;/li&gt;
&lt;li&gt;leaves no orphaned processes running&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This prevents multiple tunnels from piling up over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chromium Integration
&lt;/h2&gt;

&lt;p&gt;The script checks whether Chromium is already running.&lt;br&gt;
&lt;strong&gt;If it is:&lt;/strong&gt; a new tab is opened.&lt;br&gt;
&lt;strong&gt;Otherwise:&lt;/strong&gt; Chromium starts in a new window.&lt;/p&gt;

&lt;p&gt;When Chromium was launched by the script, it waits until the browser is closed before terminating the SSH tunnel.&lt;/p&gt;

&lt;p&gt;This makes the tunnel exist only for as long as it is actually needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Expose the Web Interface?
&lt;/h2&gt;

&lt;p&gt;Many Raspberry Pi tutorials suggest binding services to &lt;code&gt;0.0.0.0&lt;/code&gt;, or opening firewall ports.&lt;/p&gt;

&lt;p&gt;Personally, I prefer keeping administrative interfaces accessible only via SSH because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no additional firewall rules are required&lt;/li&gt;
&lt;li&gt;the service never becomes reachable from the LAN&lt;/li&gt;
&lt;li&gt;authentication is handled by SSH&lt;/li&gt;
&lt;li&gt;all traffic is encrypted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a simple security improvement that costs almost nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Services
&lt;/h2&gt;

&lt;p&gt;The same approach works for almost any web application running on your Raspberry Pi.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pi-hole&lt;/li&gt;
&lt;li&gt;Fail2Ban GUI&lt;/li&gt;
&lt;li&gt;Home Assistant&lt;/li&gt;
&lt;li&gt;Grafana&lt;/li&gt;
&lt;li&gt;Prometheus&lt;/li&gt;
&lt;li&gt;Node-RED&lt;/li&gt;
&lt;li&gt;Portainer&lt;/li&gt;
&lt;li&gt;Gitea&lt;/li&gt;
&lt;li&gt;Admin dashboards&lt;/li&gt;
&lt;li&gt;Internal development tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simply change the remote port and URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Possible Improvements
&lt;/h2&gt;

&lt;p&gt;Depending on your workflow, you could extend the script with features such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;automatic SSH key detection&lt;/li&gt;
&lt;li&gt;configurable command-line parameters&lt;/li&gt;
&lt;li&gt;support for multiple Raspberry Pis&lt;/li&gt;
&lt;li&gt;launching Firefox instead of Chromium&lt;/li&gt;
&lt;li&gt;automatic port availability checks&lt;/li&gt;
&lt;li&gt;desktop launcher integration&lt;/li&gt;
&lt;li&gt;support for multiple forwarded services simultaneously.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;SSH port forwarding is one of those Unix/Linux features that quietly solves a common problem in a secure and elegant way.&lt;/p&gt;

&lt;p&gt;With a small Bash script, you can turn what would normally be several manual steps into a seamless workflow: start the tunnel, open the browser, work as usual, and let the script clean everything up when you're finished.&lt;/p&gt;

&lt;p&gt;If you administer Raspberry Pi systems regularly, it's a simple quality-of-life improvement that also helps keep your administrative interfaces private and secure.&lt;/p&gt;

</description>
      <category>pihole</category>
      <category>ssh</category>
      <category>bash</category>
      <category>linux</category>
    </item>
    <item>
      <title>After 20 Years of PHP, I Built the Framework I Always Wanted</title>
      <dc:creator>Steffen Haase</dc:creator>
      <pubDate>Sun, 05 Jul 2026 11:44:12 +0000</pubDate>
      <link>https://dev.to/steffen_haase_f0b7bbb25e0/after-20-years-of-php-i-built-the-framework-i-always-wanted-4d1h</link>
      <guid>https://dev.to/steffen_haase_f0b7bbb25e0/after-20-years-of-php-i-built-the-framework-i-always-wanted-4d1h</guid>
      <description>&lt;p&gt;For more than 20 years I've worked as a PHP developer, building everything from small websites to larger business applications.&lt;/p&gt;

&lt;p&gt;During that time I've worked extensively with vanilla PHP, Laravel, and Symfony.&lt;/p&gt;

&lt;p&gt;Both Laravel and Symfony are fantastic frameworks. They have large communities, mature ecosystems, and solve problems that would otherwise take months to implement yourself. This isn't an article about replacing them.&lt;/p&gt;

&lt;p&gt;It's about why I decided to build something different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The projects I kept building&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Over the years I noticed a recurring pattern.&lt;/p&gt;

&lt;p&gt;Many of my projects didn't need an enormous ecosystem. They needed a clean starting point with the essentials already in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routing&lt;/li&gt;
&lt;li&gt;Dependency Injection&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Templating&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;Database migrations&lt;/li&gt;
&lt;li&gt;CSRF protection&lt;/li&gt;
&lt;li&gt;Rate limiting&lt;/li&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to start building the application immediately instead of spending time deciding which packages, starter kits, or frontend stack I should use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I found frustrating&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel offers several excellent starter kits, but choosing one often means choosing a frontend ecosystem as well.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do I use React?&lt;/li&gt;
&lt;li&gt;Vue?&lt;/li&gt;
&lt;li&gt;Svelte?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That usually brings Node.js, npm, a build pipeline, and another set of tools into the project.&lt;/p&gt;

&lt;p&gt;Those tools are great when they're needed. But not every project needs them.&lt;/p&gt;

&lt;p&gt;Sometimes I simply want to build a server-rendered application using modern PHP and Twig without introducing an entire JavaScript toolchain.&lt;/p&gt;

&lt;p&gt;I found myself asking the same question repeatedly:&lt;/p&gt;

&lt;p&gt;"Why isn't there a modern PHP application starter that stays focused on PHP?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So I started building one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That project eventually became Beacon. My goal wasn't, and will never be, to compete with Laravel or Symfony.&lt;/p&gt;

&lt;p&gt;My goal was much simpler ...&lt;/p&gt;

&lt;p&gt;Create a modern PHP application framework that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy to install&lt;/li&gt;
&lt;li&gt;Easy to understand&lt;/li&gt;
&lt;li&gt;Easy to configure&lt;/li&gt;
&lt;li&gt;Built around modern PHP features&lt;/li&gt;
&lt;li&gt;Secure by default&lt;/li&gt;
&lt;li&gt;Suitable for real-world applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of depending on dozens of packages, I wanted a coherent foundation that includes the features I reach for in almost every project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Beacon includes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Beacon comes with many of the things I consider essential:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependency Injection&lt;/li&gt;
&lt;li&gt;Attribute-based routing&lt;/li&gt;
&lt;li&gt;Middleware&lt;/li&gt;
&lt;li&gt;Twig templating&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;CSRF protection&lt;/li&gt;
&lt;li&gt;Rate limiting&lt;/li&gt;
&lt;li&gt;Database migrations&lt;/li&gt;
&lt;li&gt;CLI tooling&lt;/li&gt;
&lt;li&gt;Strong typing throughout&lt;/li&gt;
&lt;li&gt;PHP 8.5+ support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The emphasis has always been on clarity rather than cleverness.&lt;/p&gt;

&lt;p&gt;When I revisit code six months later, I want to understand exactly what it's doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building a framework has given me a much deeper appreciation for the engineering behind projects like Laravel and Symfony.&lt;/p&gt;

&lt;p&gt;Features that seem straightforward—routing, dependency injection, middleware, sessions, authentication, validation—become surprisingly complex once you build them yourself.&lt;/p&gt;

&lt;p&gt;It's been one of the most rewarding software engineering projects I've ever worked on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm looking for feedback&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Beacon is open source, and I'd genuinely appreciate feedback from other PHP developers.&lt;/p&gt;

&lt;p&gt;I'm not trying to convince anyone to abandon Laravel or Symfony.&lt;/p&gt;

&lt;p&gt;Instead, I'm interested in discussing the design decisions, architecture, and trade-offs behind building a modern PHP application framework.&lt;/p&gt;

&lt;p&gt;If Beacon sounds interesting, I'd love to hear your thoughts, suggestions, and constructive criticism.&lt;/p&gt;

&lt;p&gt;But please keep in mind, Beacon is still work-in-progress and there are still some features in my development pipeline, which I haven't implemented (or finished) yet.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/SHWorX/beacon" rel="noopener noreferrer"&gt;https://github.com/SHWorX/beacon&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>php</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
