<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Unstable Muffin - how-to</title><link href="https://blog.unstablemuffin.com/" rel="alternate"/><link href="https://blog.unstablemuffin.com/feeds/how-to.tag.atom.xml" rel="self"/><id>https://blog.unstablemuffin.com/</id><updated>2026-04-21T00:00:00-07:00</updated><entry><title>How To: Migrate a PostgreSQL database</title><link href="https://blog.unstablemuffin.com/how-to-migrate-a-postgresql-database.html" rel="alternate"/><published>2026-04-21T00:00:00-07:00</published><updated>2026-04-21T00:00:00-07:00</updated><author><name>Michael</name></author><id>tag:blog.unstablemuffin.com,2026-04-21:/how-to-migrate-a-postgresql-database.html</id><summary type="html">&lt;p&gt;How to migrate a PostgreSQL database from one server to&amp;nbsp;another&lt;/p&gt;</summary><content type="html">&lt;p&gt;This article will show you how to migrate a database from one PostgreSQL server to&amp;nbsp;another.&lt;/p&gt;
&lt;p&gt;Before starting the database migration, stop the service that is accessing the database to prevent a corrupt backup and/or missing data. This should also flush any cached data to the&amp;nbsp;disk.&lt;/p&gt;
&lt;h3 id="on-the-old-server"&gt;On the old&amp;nbsp;server&lt;/h3&gt;
&lt;p&gt;Dump the database contents from the old database to a&amp;nbsp;file.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pg_dump&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;-h&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;ip_address&amp;gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-U&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;user&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;database&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;backup_file&amp;gt;.sql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_dump&lt;/code&gt; &amp;ndash; This is a PostgreSQL utility command that dumps all the data from a PostgreSQL database to a file. This includes not only the data but also indexes, schema, autoincrement states, and everything else that represents the state of the database at the time the backup was&amp;nbsp;made.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-h &amp;lt;ip_address&amp;gt;&lt;/code&gt; &amp;ndash; This specifies where to find the PostgreSQL database to dump from. If this command is run on the same machine as the database, it can be omitted and will default to the current&amp;nbsp;machine. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;-U &amp;lt;user&amp;gt;&lt;/code&gt; &amp;ndash; This is the username of the &lt;span class="caps"&gt;SQL&lt;/span&gt; user to access the database. Make sure it has permissions to access the&amp;nbsp;database.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d &amp;lt;database&amp;gt;&lt;/code&gt; &amp;ndash; The name of the database to&amp;nbsp;backup.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-f &amp;lt;backup_file&amp;gt;.sql&lt;/code&gt; &amp;ndash; This specifies the name of the file to dump the database to. Note that the output of this command is plaintext &lt;span class="caps"&gt;SQL&lt;/span&gt; statements, so the file extension should&amp;nbsp;be &lt;code&gt;.sql&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The contents of the backup file is plaintext &lt;span class="caps"&gt;SQL&lt;/span&gt;, so you can view and modify the backup if desired. This can be helpful to adjust usernames, database names, and permissions if you want them to be different on the new server than the old one. When the backup is restored, it will execute every line in the file one-by-one to recreated the database. Any changes to the backup will be executed as&amp;nbsp;well.&lt;/p&gt;
&lt;h3 id="on-the-new-server"&gt;On the new&amp;nbsp;server&lt;/h3&gt;
&lt;p&gt;First, connect to the new&amp;nbsp;database.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;postgres&lt;span class="w"&gt; &lt;/span&gt;psql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Create the database&amp;nbsp;user.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;password&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Create the&amp;nbsp;database.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DATABASE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;db_name&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OWNER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, grant the new user all permissions and privileges on the newly created&amp;nbsp;database.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;GRANT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ALL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SCHEMA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once the user and database are created in the new server, you can exit the PostgreSQL command line&amp;nbsp;(&lt;code&gt;\q&lt;/code&gt;). The next step is to configure PostgreSQL to allow access to the new database for this user. Open the access configuration file for PostgreSQL in any&amp;nbsp;editor.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;nano&lt;span class="w"&gt; &lt;/span&gt;/etc/postgresql/&amp;lt;version&amp;gt;/main/pg_hba.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And add a line like the&amp;nbsp;following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TYPE&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nx"&gt;DATABASE&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;USER&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;ADDRESS&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;METHOD&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;db_user&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;auth_type&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;type&amp;gt;&lt;/code&gt; &amp;ndash; This specifies the type of connection. Valid options&amp;nbsp;include &lt;code&gt;local&lt;/code&gt;, &lt;code&gt;host&lt;/code&gt;, &lt;code&gt;hostssl&lt;/code&gt;,&amp;nbsp;and &lt;code&gt;hostnossl&lt;/code&gt;. &lt;code&gt;local&lt;/code&gt; is for connections over the PostgreSQL Unix&amp;nbsp;socket. &lt;code&gt;host&lt;/code&gt; is for &lt;span class="caps"&gt;TCP&lt;/span&gt;/&lt;span class="caps"&gt;IP&lt;/span&gt; connections (either remote or via&amp;nbsp;localhost). &lt;code&gt;hostssl&lt;/code&gt; and &lt;code&gt;hostnossl&lt;/code&gt; are for specifically &lt;span class="caps"&gt;TCP&lt;/span&gt;/&lt;span class="caps"&gt;IP&lt;/span&gt; conections that are required to have &lt;span class="caps"&gt;SSL&lt;/span&gt; encryption or&amp;nbsp;not. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;database&amp;gt;&lt;/code&gt; &amp;ndash; This is the name of the database this record allows access&amp;nbsp;to.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;db_user&amp;gt;&lt;/code&gt; &amp;ndash; This is the user that is being allowed access to the above&amp;nbsp;database.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;ip_address&amp;gt;&lt;/code&gt; &amp;ndash; This is the &lt;span class="caps"&gt;IP&lt;/span&gt; address from which the specified user is allowed to connect to the specified database. The format should be &lt;span class="caps"&gt;IP&lt;/span&gt; address and subnet. A subnet&amp;nbsp;of &lt;code&gt;/32&lt;/code&gt; corresponds to a single &lt;span class="caps"&gt;IP&lt;/span&gt; address, so something&amp;nbsp;like &lt;code&gt;127.0.0.1/32&lt;/code&gt; would correspond to only allowing connections from localhost over IPv4. For access from Docker containers, which have dynamic &lt;span class="caps"&gt;IP&lt;/span&gt; addresses, you should whitelist the entire Docker&amp;nbsp;subnet: &lt;code&gt;172.22.0.0/16&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;auth_type&amp;gt;&lt;/code&gt; &amp;ndash; This is the type of authentication required for this method of access. Options&amp;nbsp;include &lt;code&gt;trust&lt;/code&gt;, &lt;code&gt;reject&lt;/code&gt;, &lt;code&gt;md5&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;scram-sha-256&lt;/code&gt;,&amp;nbsp;and &lt;code&gt;peer&lt;/code&gt;, among&amp;nbsp;others.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trust&lt;/code&gt; will implicitly trust this connection and not require any authentication at&amp;nbsp;all. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;reject&lt;/code&gt; will reject this connection. This is useful in conjuction with other records, which allow you to fine tune access. Records are evaluated from top to bottom and terminate on the first match, so you can reject a specific &lt;span class="caps"&gt;IP&lt;/span&gt; address at the top of the file, and then accept a broader range of &lt;span class="caps"&gt;IP&lt;/span&gt; addresses lower in the&amp;nbsp;file. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;md5&lt;/code&gt; will accept a password with &lt;span class="caps"&gt;SCRAM&lt;/span&gt;-&lt;span class="caps"&gt;SHA&lt;/span&gt;-256 &lt;em&gt;or&lt;/em&gt; &lt;span class="caps"&gt;MD5&lt;/span&gt; hashing algorithm. &lt;span class="caps"&gt;MD5&lt;/span&gt; is easily cracked with modern machines and is not considered secure. This authentication method is deprecated and will be removed in future version of&amp;nbsp;PostgreSQL.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;password&lt;/code&gt; requires the client to send their password unencrypted to the server. The password is transmitted in cleartext, so this authentication method is not recommended on untrusted&amp;nbsp;networks. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;scram-sha-256&lt;/code&gt; will required strong encryption for the&amp;nbsp;password. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;peer&lt;/code&gt; authentication will check if the user connecting has an &lt;span class="caps"&gt;OS&lt;/span&gt; account that matches the name of a database user. If so, it will allow the connection. This authentication method is only valid&amp;nbsp;on &lt;code&gt;local&lt;/code&gt; connection&amp;nbsp;types.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you intend to allow connections from other devices or networks, you must change PostgreSQL&amp;rsquo;s configuration to bind to all network interfaces. By default it is only bound to localhost, and connections from another network (e.g. another computer or even Docker running on the same machine) will&amp;nbsp;fail. &lt;/p&gt;
&lt;p&gt;To configure PostgreSQL to bind to all available interfaces, modify the config file&amp;nbsp;at &lt;code&gt;/etc/postgresql/&amp;lt;version&amp;gt;/main/postgresql.conf&lt;/code&gt; and change the following&amp;nbsp;line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;listen_addresses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sc"&gt;&amp;#39;*&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After saving the changes to the configuration, trigger a reload to force PostgreSQL to update its configuration. If you changed&amp;nbsp;the &lt;code&gt;listen_addresses&lt;/code&gt; config, you&amp;nbsp;must &lt;code&gt;restart&lt;/code&gt; the PostgreSQL service instead of just reloading&amp;nbsp;it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;reload&lt;span class="w"&gt; &lt;/span&gt;postgresql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, restore the database backup to the new&amp;nbsp;database.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;psql&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;-h&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;ip_address&amp;gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-U&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;user&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;database&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;backup_file&amp;gt;.sql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;psql&lt;/code&gt; &amp;ndash; This is the command that connects to a PostgreSQL&amp;nbsp;server.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-h &amp;lt;ip_address&amp;gt;&lt;/code&gt; &amp;ndash; Host of the new database server. If the backup file was copied to the new server and this command is being run on the same device as the database being restored to, this can be&amp;nbsp;omitted.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-U &amp;lt;user&amp;gt;&lt;/code&gt; &amp;ndash; The username to connect with. Should be the same as the old&amp;nbsp;one.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d &amp;lt;database&amp;gt;&lt;/code&gt; &amp;ndash; The database name. Should be the same as the old&amp;nbsp;one.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-f &amp;lt;backup_file&amp;gt;.sql&lt;/code&gt; &amp;ndash; The file containing the backed up data created&amp;nbsp;from &lt;code&gt;pg_dump&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now the data should be restored to the new database. Point the service to the new database and start it up, and it should connect and appear that nothing has&amp;nbsp;changed.&lt;/p&gt;</content><category term="Technical"/><category term="how-to"/></entry><entry><title>How To: Install Steam mods on Linux</title><link href="https://blog.unstablemuffin.com/how-to-install-steam-mods-on-linux.html" rel="alternate"/><published>2026-03-18T00:00:00-07:00</published><updated>2026-03-18T00:00:00-07:00</updated><author><name>Michael</name></author><id>tag:blog.unstablemuffin.com,2026-03-18:/how-to-install-steam-mods-on-linux.html</id><summary type="html">&lt;p&gt;How to install mods for Steam games on&amp;nbsp;Linux&lt;/p&gt;</summary><content type="html">&lt;p&gt;When you install a mod for a Steam game on Linux, it may not work correctly in the game. In many cases this is due to the differences in the way Windows and Linux interpret file names. In Windows, file names are case-insensitive,&amp;nbsp;meaning &lt;code&gt;MyFile.txt&lt;/code&gt; and &lt;code&gt;myfile.txt&lt;/code&gt; refer to the same file on the disk. In contrast, on Linux file names are case sensitive, so those two file names would refer to different&amp;nbsp;files.&lt;/p&gt;
&lt;p&gt;Since Windows dominates the gaming scene, most mods are developed on Windows computers, where the casing of file names doesn&amp;rsquo;t matter. This leads to modders getting lazy with their filenames and often using a different casing than the game uses. While the mod might work without issue on Windows, on Linux the mod will fail in unexpected ways when it tries to reference files that may or may not exist (at least, as Linux sees&amp;nbsp;them).&lt;/p&gt;
&lt;p&gt;The solution to this problem is to install a virtual file system on your Linux computer that will emulate a Windows file system with case insensitive filenames. When the mod loads files from this virtual file system, it loads files with any casing correctly while the game itself continues natively on Linux. The steps below will show you how to create the virtual file system and then mount it for use with your games. You don&amp;rsquo;t have to reinstall your mods this way either; they can be copied over to the new file system and should start working&amp;nbsp;immediately.&lt;/p&gt;
&lt;h2 id="create-the-virtual-file-system"&gt;Create the virtual file&amp;nbsp;system&lt;/h2&gt;
&lt;p&gt;To start with, you need to create the virtual file system. We will be using the vFAT file system for this purpose. vFAT is a lightweight, Windows-compatible file system with native support in the Linux kernel. It&amp;rsquo;s significantly faster to use compared with a full &lt;span class="caps"&gt;NTFS&lt;/span&gt; file system, since &lt;span class="caps"&gt;NTFS&lt;/span&gt; provides a bunch of features that are not needed for a small virtual file system and it requires additional driver support in&amp;nbsp;Linux.&lt;/p&gt;
&lt;p&gt;To create the virtual file system, run this&amp;nbsp;command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;mkfs.vfat&lt;span class="w"&gt; &lt;/span&gt;-C&lt;span class="w"&gt; &lt;/span&gt;-F&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/opt/steam-workshop.img&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2097152&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mkfs.vfat&lt;/code&gt; - This command creates a virtual file system using the &lt;span class="caps"&gt;FAT&lt;/span&gt; file system&amp;nbsp;format.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-C&lt;/code&gt; - This option specifies that the command should create a file instead of using an existing physical device like a &lt;span class="caps"&gt;USB&lt;/span&gt;&amp;nbsp;stick.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-F 32&lt;/code&gt; - This option specifies using &lt;span class="caps"&gt;FAT32&lt;/span&gt; format, allowing the largest file sizes in the &lt;span class="caps"&gt;FAT&lt;/span&gt;&amp;nbsp;family.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/opt/steam-workshop.img&lt;/code&gt; - This specifies the file where you want to file system to be stored. This can be any place and any name you want. If you use a different path or name, be sure to change in the rest of the commands&amp;nbsp;below.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2097152&lt;/code&gt; - This is the size of the file system you want to create. This number is the number of file system blocks, which&amp;nbsp;the &lt;code&gt;mkfs.vfat&lt;/code&gt; command defaults to 1024 bytes each, or 1 &lt;span class="caps"&gt;KB&lt;/span&gt;. So this number should be how many kilobytes the file should be. E.g. 1 &lt;span class="caps"&gt;MB&lt;/span&gt; is 1024 &lt;span class="caps"&gt;KB&lt;/span&gt;, and 1 &lt;span class="caps"&gt;GB&lt;/span&gt; is 1048576 &lt;span class="caps"&gt;KB&lt;/span&gt;. I chose 2097152 blocks for mine, which equates to 2 &lt;span class="caps"&gt;GB&lt;/span&gt;. You can adjust this value depending on how many mods you want to be able to install at one&amp;nbsp;time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Run this command as root on your system and verify it created a new file at the specified path. It is already formatted with the &lt;span class="caps"&gt;FAT32&lt;/span&gt; file system, so next we need to use&amp;nbsp;it.&lt;/p&gt;
&lt;h2 id="mounting-the-virtual-file-system"&gt;Mounting the virtual file&amp;nbsp;system&lt;/h2&gt;
&lt;p&gt;You can&amp;rsquo;t access the virtual file system directly through the file we created in the last step; it must be mounted to the local file system as a virtual block device so it can be used by the operating system. To do that, we&amp;rsquo;ll use a loopback&amp;nbsp;device. &lt;/p&gt;
&lt;h3 id="choosing-a-loopback-device"&gt;Choosing a loopback&amp;nbsp;device&lt;/h3&gt;
&lt;p&gt;Loopback devices typically use the naming&amp;nbsp;convention &lt;code&gt;/dev/loopXX&lt;/code&gt; where &lt;span class="caps"&gt;XX&lt;/span&gt; is any numeric digit. To see if you have any loopback devices in use already, you can run the&amp;nbsp;command &lt;code&gt;losetup -l&lt;/code&gt;. If you don&amp;rsquo;t see loopback devices listed, then you are free to pick any number, or start at 0. If you do see any loopback devices, just choose a number that&amp;rsquo;s not in use. You can either use the next available number, or choose an arbitrary higher number to leave room for other programs creating loopback&amp;nbsp;devices.&lt;/p&gt;
&lt;p&gt;To create the loopback device, run the following&amp;nbsp;command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;losetup&lt;span class="w"&gt; &lt;/span&gt;/dev/loopXX&lt;span class="w"&gt; &lt;/span&gt;/opt/steam-workshop.img
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;losetup&lt;/code&gt; - This is the name of the program that creates, manages, and deletes loopback devices on your&amp;nbsp;system. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/loopXX&lt;/code&gt; - This is the loopback device to create. Replace&amp;nbsp;the &lt;code&gt;XX&lt;/code&gt; with number you chose&amp;nbsp;previously.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/opt/steam-workshop.img&lt;/code&gt; - This is the path to the virtual file system you created in the previous&amp;nbsp;section.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the command ran successfully, the virtual file system file is now running as a block device on your system. The next step is to mount it so your operating system can access the files in&amp;nbsp;it.&lt;/p&gt;
&lt;h3 id="manually-mounting-the-virtual-file-system"&gt;Manually mounting the virtual file&amp;nbsp;system&lt;/h3&gt;
&lt;p&gt;Next, you can mount the virtual file system by running the following&amp;nbsp;command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;mount&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;user&amp;gt;,gid&lt;span class="o"&gt;=&lt;/span&gt;users,fmask&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;113&lt;/span&gt;,dmask&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;002&lt;/span&gt;,iocharset&lt;span class="o"&gt;=&lt;/span&gt;iso8859-1&lt;span class="w"&gt; &lt;/span&gt;/dev/loopXX&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/path/to/steam/steamapps/workshop/content&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo mount&lt;/code&gt; - This command mounts file systems into your root file system for the operating system to use. It must always be run as the root&amp;nbsp;user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o uid=&amp;lt;user&amp;gt;,gid=users,fmask=113,dmask=002,iocharset=iso8859-1&lt;/code&gt; - This specifies the options for the file system to be mounted.&amp;nbsp;Replace &lt;code&gt;&amp;lt;user&amp;gt;&lt;/code&gt; with your username.&amp;nbsp;The &lt;code&gt;uid&lt;/code&gt; and &lt;code&gt;gid&lt;/code&gt; options specify the user and group that the file system should be mounted with.&amp;nbsp;The &lt;code&gt;fmask&lt;/code&gt; and &lt;code&gt;dmask&lt;/code&gt; options set the file permissions. And&amp;nbsp;finally &lt;code&gt;iocharset&lt;/code&gt; sets the character encoding for&amp;nbsp;filenames.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/loopXX&lt;/code&gt; - This is the loopback device you created previously.&amp;nbsp;Replace &lt;code&gt;XX&lt;/code&gt; with the number of the loopback device you&amp;nbsp;created.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"/path/to/steam/steamapps/workshop/content"&lt;/code&gt; - This is the path to the workshop content (mods + other downloadable content from Steam) on your computer. It can vary from system to system and the way Steam was installed. But once you find the directory steam is installed at, inside you should find&amp;nbsp;the &lt;code&gt;steamapps/workshop/content&lt;/code&gt; folder where any current mods for any game are installed. Note the double quotes around the path. &lt;strong&gt;If there&amp;rsquo;s already content inside this folder, be sure to back it up before mounting this directory!&lt;/strong&gt; It&amp;rsquo;s as simple&amp;nbsp;as &lt;code&gt;cp -r content/ content_old/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After running this command, the virtual file system should be mounted in Steam&amp;rsquo;s content directory, meaning any files the Steam workshop downloads to your computer will be stored in the virtual file system instead of on your&amp;nbsp;disk. &lt;/p&gt;
&lt;p&gt;If you backed up existing files from the content directory, you can use this command to restore those&amp;nbsp;files:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cp&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;content_old/*&lt;span class="w"&gt; &lt;/span&gt;content/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-a&lt;/code&gt; flag will recursively copy all files, preserving file permissions, ownership, timestamps, and extended attributes. Just make sure&amp;nbsp;the &lt;code&gt;content/&lt;/code&gt; directory is empty, as this command will overwrite any files with the same&amp;nbsp;name. &lt;/p&gt;
&lt;h3 id="automatically-mounting-the-file-system"&gt;Automatically mounting the file&amp;nbsp;system&lt;/h3&gt;
&lt;p&gt;If you followed the instructions previously to manually mount the virtual file system, you will notice that it will become unmounted every time you turn off the computer. You can create a systemd service to automatically mount the file system at&amp;nbsp;boot. &lt;/p&gt;
&lt;p&gt;First, create the service file using your favorite text editor. I&amp;rsquo;m using nano because it&amp;rsquo;s objectively the best. The file can be named whatever you want, as long as it ends&amp;nbsp;with &lt;code&gt;.service&lt;/code&gt; and can be found in&amp;nbsp;the &lt;code&gt;/etc/systemd/system/&lt;/code&gt; directory. If you use a different service name, be sure to change the name in the rest of the&amp;nbsp;commands.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;nano&lt;span class="w"&gt; &lt;/span&gt;/etc/systemd/system/steam-workshop.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And add the following content to&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Steam Workshop virtual drive device and mount&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;local-fs.target&lt;/span&gt;
&lt;span class="na"&gt;Before&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;graphical.target&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/bin/bash -c &amp;#39;/sbin/losetup /dev/loopXX /opt/steam-workshop.img &amp;amp;&amp;amp; /bin/mount -o uid=&amp;lt;usr&amp;gt;,gid=users,fmask=113,dmask=002,iocharset=iso8859-1 /dev/loopXX &amp;quot;/path/to/steam/steamapps/workshop/content&amp;quot;&amp;#39;&lt;/span&gt;
&lt;span class="na"&gt;ExecStop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/bin/bash -c &amp;#39;/bin/umount &amp;quot;/path/to/steam/steamapps/workshop/content&amp;quot; &amp;amp;&amp;amp; /sbin/losetup -d /dev/loopXX&amp;#39;&lt;/span&gt;
&lt;span class="na"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here are the important things to note about this service&amp;nbsp;file:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Description=&lt;/code&gt; - The description for this systemd service. You can put anything&amp;nbsp;here.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;After=local-fs.target&lt;/code&gt; - This will prevent this service from starting until after the local file system has been initialized. This is important because we are loading from an existing file on the local file system, so we have to make sure the local file system is fully initialized before mounting the virtual file&amp;nbsp;system.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Before=graphical.target&lt;/code&gt; - This specifies that the graphical subsystem should not start until this service completes. That means the virtual file system should be mounted before the graphics system is initialized. This is helpful if you have Steam set to start at system startup: it will prevent Steam from auto-starting before the virtual file system has been mounted, causing Steam to see an&amp;nbsp;empty &lt;code&gt;content/&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Type=oneshot&lt;/code&gt; - This specifies that this command will run a single command and terminate (as opposed to a daemon that runs forever). This informs systemd how to manage and report the status of this&amp;nbsp;service.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExecStart=&lt;/code&gt; - This is what systemd will run when it starts your service. It should look similar to the steps to manually mount the virtual file system. &lt;strong&gt;Things to note about this command:&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;It combines both the creation of the loopback device and mounting it into one command, joined&amp;nbsp;by &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;. The whole thing is wrapped in a call&amp;nbsp;to &lt;code&gt;/bin/bash -c '...'&lt;/code&gt; because systemd can only run one command. Note the single quotes; since the command uses double quotes around the file path, we use single quotes here to not interfere with&amp;nbsp;those.&lt;/li&gt;
&lt;li&gt;We use the full path&amp;nbsp;to &lt;code&gt;/sbin/losetup&lt;/code&gt; instead of just the command name itself. Since this is running early on in the boot process, it&amp;rsquo;s safer to refer to the executable directly instead of relying on the &lt;span class="caps"&gt;PATH&lt;/span&gt; environment variable being set up&amp;nbsp;correctly.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExecStop=&lt;/code&gt; - This is the command that&amp;rsquo;s run when systemd attempts to stop this service; either automatically or when you&amp;nbsp;run &lt;code&gt;systemctl stop steam-workshop.service&lt;/code&gt; (or whatever you named the service). It&amp;rsquo;s the same steps&amp;nbsp;in &lt;code&gt;ExecStart&lt;/code&gt; but opposite and in reverse order: unmounting the virtual file system and destroying the loopback device. This allows systemd to cleanly shutdown and free those resources either during system shutdown or if you manually specify it on the command line. Without this step you may be stuck with leftover resources not cleaned up preventing you from remounting&amp;nbsp;things.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RemainAfterExit=yes&lt;/code&gt; - This tells systemd that the service will exit after starting but to mark it as still active (as long as it ran the startup command successfully). This will cause systemd to report the status of this service correctly, as well as making sure&amp;nbsp;the &lt;code&gt;ExecStop&lt;/code&gt; command is run during shutdown. Without this command systemd would consider this service inactive after it finishes&amp;nbsp;running &lt;code&gt;ExecStart&lt;/code&gt; and wouldn&amp;rsquo;t&amp;nbsp;run &lt;code&gt;ExecStop&lt;/code&gt; later.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WantedBy=multi-user.target&lt;/code&gt; - This indicates to systemd when to run this script during boot&amp;nbsp;up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After creating the service, enable and start it&amp;nbsp;with &lt;code&gt;sudo systemctl enable --now steam-workshop.service&lt;/code&gt;. Now the virtual file system will be created and mounted automatically when the computer boots up, ready for Steam to use. Since it&amp;rsquo;s mounted in the directory Steam expects workspace content to be, any newly subscribed content will automatically be added to the virtual file&amp;nbsp;system.&lt;/p&gt;</content><category term="Technical"/><category term="how-to"/></entry><entry><title>How To: Set up a reverse proxy</title><link href="https://blog.unstablemuffin.com/how-to-set-up-a-reverse-proxy.html" rel="alternate"/><published>2026-03-16T00:00:00-07:00</published><updated>2026-03-16T00:00:00-07:00</updated><author><name>Michael</name></author><id>tag:blog.unstablemuffin.com,2026-03-16:/how-to-set-up-a-reverse-proxy.html</id><summary type="html">&lt;p&gt;How to set up public access to services on your home&amp;nbsp;network&lt;/p&gt;</summary><content type="html">&lt;p&gt;Setting up external access to servers inside your home network is possible by setting up reverse tunnels behind a reverse&amp;nbsp;proxy.&lt;/p&gt;
&lt;p&gt;You will need an external server that is publicly accessible by the Internet. This server can serve any number of private servers inside your network via a reverse proxy. I personally use a basic DigitalOcean droplet that costs $6 per month. You don&amp;rsquo;t need a lot of power for this server, but you want something that won&amp;rsquo;t max out your bandwidth or &lt;span class="caps"&gt;CPU&lt;/span&gt; limits, as that will introduce a lot of latency to your &lt;span class="caps"&gt;SSH&lt;/span&gt; tunnels and ultimately your&amp;nbsp;applications.&lt;/p&gt;
&lt;p&gt;You will need to configure two components to enable access to your home network. First, set up the reverse tunnel to tunnel traffic from the remote server to the local server. Then, configure the reverse proxy on the remote server to direct traffic to the reverse&amp;nbsp;tunnel.&lt;/p&gt;
&lt;h2 id="definitions"&gt;Definitions&lt;/h2&gt;
&lt;dl&gt;
&lt;dt&gt;External/Remote&amp;nbsp;server&lt;/dt&gt;
&lt;dd&gt;This is the server that is running outside of your network and is publicly accessible from the&amp;nbsp;Internet.&lt;/dd&gt;
&lt;dt&gt;Local/Private&amp;nbsp;server&lt;/dt&gt;
&lt;dd&gt;This is a server running inside of your home network. You can have multiple local servers, in which you would follow these instructions for each tunnel on each&amp;nbsp;server.&lt;/dd&gt;
&lt;dt&gt;Reverse&amp;nbsp;tunnel&lt;/dt&gt;
&lt;dd&gt;This is an &lt;span class="caps"&gt;SSH&lt;/span&gt; tunnel that is established from the server that the traffic is being forwarded to. This is the opposite of a traditional tunnel which is initiated from the server sending the traffic, hence the name reverse tunnel. Reverse tunnels are useful when the destination of the traffic is inaccessible from the source of the traffic, such as a home network that has inbound traffic blocked by an &lt;span class="caps"&gt;ISP&lt;/span&gt;.&lt;/dd&gt;
&lt;dt&gt;Reverse&amp;nbsp;proxy&lt;/dt&gt;
&lt;dd&gt;This is a web server that listens for &lt;span class="caps"&gt;HTTP&lt;/span&gt;/s traffic and redirects (i.e. proxies) that traffic to a different endpoint. In this setup, the reverse proxy helps isolate your network and tunnel from potentially dangerous traffic on the Internet. A traditional proxy is a web server that processes outbound traffic, hence why one that processes inbound traffic is called a reverse&amp;nbsp;proxy.&lt;/dd&gt;
&lt;/dl&gt;
&lt;h2 id="reverse-ssh-tunnel"&gt;Reverse &lt;span class="caps"&gt;SSH&lt;/span&gt;&amp;nbsp;Tunnel&lt;/h2&gt;
&lt;p&gt;The reverse tunnel creates an &lt;span class="caps"&gt;SSH&lt;/span&gt; session to a remote machine and tunnels traffic from the remote machine to the local machine. You will need one reverse tunnel for each port you want to forward traffic to. You can have as many tunnels as you need from the same&amp;nbsp;machine.&lt;/p&gt;
&lt;h3 id="setting-up-the-reverse-tunnel"&gt;Setting up the reverse&amp;nbsp;tunnel&lt;/h3&gt;
&lt;p&gt;Use the following ssh command from the local server to create the reverse ssh&amp;nbsp;tunnel:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;/usr/bin/ssh&lt;span class="w"&gt; &lt;/span&gt;-N&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ServerAliveInterval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ServerAliveCountMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ExitOnForwardFailure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes&lt;span class="w"&gt; &lt;/span&gt;-R&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;remote-listening-port&amp;gt;:&amp;lt;local-forwarding-server&amp;gt;:&amp;lt;local-forwarding-port&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;path-to-private-key&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;remote-username&amp;gt;@&amp;lt;remote-server&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-N&lt;/code&gt;: This option tells &lt;span class="caps"&gt;SSH&lt;/span&gt; not to set up a full shell session for this connection. This will only establish the &lt;span class="caps"&gt;SSH&lt;/span&gt; connection for the&amp;nbsp;tunnel.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o ServerAliveInterval=60&lt;/code&gt;: This option sends a keep-alive signal through the tunnel every 60 seconds. You may need this option if the tunnel is closed due to&amp;nbsp;inactivity.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o ServerAliveCountMax=3&lt;/code&gt;: This option tells the connection to terminate when 3 keep-alive signals are not responded to. This will automatically close the tunnel if the remote machine has disconnected or crashed without closing the connection. Without this option, the tunnel may stay open but unconnected and will need to be manually restarted in order to reconnect to the remote server. In addition, when used in a systemd service (&lt;a href="#creating-a-service-for-the-tunnel"&gt;covered in a later section&lt;/a&gt;), this allows the tunnel to automatically restart&amp;nbsp;itself.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o ExitOnForwardFailure=yes&lt;/code&gt;: This option tells &lt;span class="caps"&gt;SSH&lt;/span&gt; to exit with a failure if it can&amp;rsquo;t establish the connection. This allows it to automatically retry when inside of a systemd&amp;nbsp;service.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-R &amp;lt;remote-listening-port&amp;gt;:&amp;lt;local-forwarding-server&amp;gt;:&amp;lt;local-forwarding-port&amp;gt;&lt;/code&gt;: This is the option for establishing the reverse tunnel.
The tunnel will be set up to listen on&amp;nbsp;port &lt;code&gt;&amp;lt;remote-listening-port&amp;gt;&lt;/code&gt; on the remote server, and all traffic that arrives at that port will be forwarded&amp;nbsp;to &lt;code&gt;&amp;lt;local-forwarding-server&amp;gt;&lt;/code&gt; on&amp;nbsp;port &lt;code&gt;&amp;lt;local-forwarding-port&amp;gt;&lt;/code&gt; in your home network. &lt;br&gt;&lt;br&gt;
Note that by default, the remote port is bound to localhost, so it is not accessible to the Internet. You can choose to bind it to a public address by specifying a remote server address to bind to (for&amp;nbsp;example, &lt;code&gt;-R 0.0.0.0:&amp;lt;remote-listening-port&amp;gt;:&amp;lt;local-forwarding-server&amp;gt;:&amp;lt;local-forwarding-port&amp;gt;&lt;/code&gt; would allow any traffic to access the tunnel) but this is generally highly discouraged because you&amp;rsquo;re opening a direct connection from the Internet into your home network. There are bots constantly scanning the Internet for open ports to exploit, and then once an application is compromised they will move laterally inside your home network to compromise the rest of your devices. It&amp;rsquo;s recommended to keep the tunnels bound to localhost and run them behind a reverse proxy, which will be covered in the &lt;a href="#reverse-proxy"&gt;next section&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-i &amp;lt;path-to-private-key&amp;gt;&lt;/code&gt;: This option specifies the path to the private key file to use to authenticate the connection. You can use a different form of authentication if you want, for example username and password, but it&amp;rsquo;s highly recommended not to have your username or password hard coded, and key pair authentication is easier to use for unattended&amp;nbsp;services.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;remote-username&amp;gt;@&amp;lt;remote-server&amp;gt;&lt;/code&gt;: Finally, this is the username and server (either &lt;span class="caps"&gt;IP&lt;/span&gt; address or domain name) of the remote server. The user should be a special user used just for your &lt;span class="caps"&gt;SSH&lt;/span&gt; tunnels and have all other permissions and logon rights disabled for security. Instruction on how to set this user up will be covered in a &lt;a href="#creating-a-user-for-the-tunnel"&gt;later section&lt;/a&gt;. However, you can also use just a regular logon user for this, it&amp;rsquo;s just less&amp;nbsp;secure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is an example that I am currently using to create a tunnel from port 3001 on a public DigitalOcean droplet to an instance of Jellyfin running on the same machine on port&amp;nbsp;8096:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;/usr/bin/ssh&lt;span class="w"&gt; &lt;/span&gt;-N&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ServerAliveInterval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ServerAliveCountMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ExitOnForwardFailure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes&lt;span class="w"&gt; &lt;/span&gt;-R&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3001&lt;/span&gt;:localhost:8096&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;/home/sshtunnel/.ssh/tunnel_rsa&lt;span class="w"&gt; &lt;/span&gt;sshtunnel@cloud.michaelhumphrey.dev
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="creating-a-service-for-the-tunnel"&gt;Creating a service for the&amp;nbsp;tunnel&lt;/h3&gt;
&lt;p&gt;If you run the reverse tunnel from the command line, it will only stay running as long as the command is running. If you close the terminal it&amp;rsquo;s running in, the reverse tunnel will terminate. You can always start the command and run it in the background, but then you may not notice if the tunnel shuts down, and is difficult to restart. To fix these issues, you can configure a systemd service to manage the tunnel for you. Below is a template you can use to create the&amp;nbsp;service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;service-name&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;username&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/ssh -N -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes -R &amp;lt;remote-listening-port&amp;gt;:&amp;lt;local-forwarding-server&amp;gt;:&amp;lt;local-forwarding-port&amp;gt; -i &amp;lt;path-to-private-key&amp;gt; &amp;lt;remote-username&amp;gt;@&amp;lt;remote-server&amp;gt;&lt;/span&gt;

&lt;span class="na"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Description=&amp;lt;service-name&amp;gt;&lt;/code&gt;: A short description of this&amp;nbsp;tunnel.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;After=network-online.target&lt;/code&gt;: Wait to start this service until after the network is&amp;nbsp;online.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;User=&amp;lt;username&amp;gt;&lt;/code&gt;: The user this service should run under. Ideally this would be a user that is only used to run the &lt;span class="caps"&gt;SSH&lt;/span&gt; tunnels and have no other permissions and logon rights disabled, but it can also be a regular user. See the &lt;a href="#creating-a-user-for-the-tunnel"&gt;next section&lt;/a&gt; for instructions on how to set up a user for the&amp;nbsp;tunnel.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExecStart=&amp;lt;ssh-command&amp;gt;&lt;/code&gt;: This is the command to be run when this service is started. See the &lt;a href="#setting-up-the-reverse-tunnel"&gt;previous section&lt;/a&gt; for instruction on how to set up the ssh&amp;nbsp;command.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RestartSec=5&lt;/code&gt;: This option specifies that if this service stops for any reason, it should wait 5 seconds before restarting. You can set this to whatever you like, but it should be short enough to minimize downtime if the tunnel can be reestablished and long enough to let the network connection shutdown and cleanup its resources before trying again. For less critical application, you can set this upward of 30-60 seconds or more to avoid straining your server if the remote server becomes unavailable for a period of time. Don&amp;rsquo;t set this to anything less than two seconds, otherwise the service may be terminated for starting up too many times too&amp;nbsp;quickly.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Restart=always&lt;/code&gt;: This option specifies that the service should always start up after it stops for any reason. You can also set this option&amp;nbsp;to &lt;code&gt;on-failure&lt;/code&gt; to only start back up if it&amp;nbsp;crashed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="creating-a-user-for-the-tunnel"&gt;Creating a user for the&amp;nbsp;tunnel&lt;/h3&gt;
&lt;p&gt;To minimize the attack vector of exposing a tunnel to your home network from the Internet, it is recommended to set up a dedicated user with minimal permissions to run the tunnel. This ensures that even if that account is compromised, attackers still don&amp;rsquo;t have access to any other systems. You only need one user for all of your tunnels to use. In these examples, I&amp;rsquo;m&amp;nbsp;using &lt;code&gt;sshtunnel&lt;/code&gt; as the username, but you can use any username you&amp;nbsp;prefer.&lt;/p&gt;
&lt;h4 id="on-the-remote-server"&gt;On the remote&amp;nbsp;server&lt;/h4&gt;
&lt;p&gt;First, create the user and required directories on the remote&amp;nbsp;server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;useradd&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;/usr/sbin/nologin&lt;span class="w"&gt; &lt;/span&gt;sshtunnel
sudo&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/home/sshtunnel/.ssh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useradd&lt;/code&gt;: This command creates the user that the tunnel will run&amp;nbsp;under.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-m&lt;/code&gt;: This option creates a home directory for the new user. We will use this directory to store the scripts for running the tunnel and symlinking them to the appropriate system&amp;nbsp;directories.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-s /usr/sbin/nologin&lt;/code&gt;: This option sets the user&amp;rsquo;s login shell&amp;nbsp;to &lt;code&gt;/usr/sbin/nologin&lt;/code&gt;. This is a special shell that disallows this user from logging into the system, but lets it run system services and create tunnels. This prevents this user from being commandeered by an attacker while still allowing us to authenticate with a minimum-permission account for setting up the&amp;nbsp;tunnel.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkdir&lt;/code&gt; This command create the directory where the user&amp;rsquo;s ssh credentials will&amp;nbsp;live.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p&lt;/code&gt;: This option creates all parent directories for this directory if they don&amp;rsquo;t already&amp;nbsp;exist.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next, ensure that the directories belong to the user we created, not the currently logged in user or&amp;nbsp;root:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;chown&lt;span class="w"&gt; &lt;/span&gt;-R&lt;span class="w"&gt; &lt;/span&gt;sshtunnel:sshtunnel&lt;span class="w"&gt; &lt;/span&gt;~/sshtunnel/.ssh
sudo&lt;span class="w"&gt; &lt;/span&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;700&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/sshtunnel/.ssh
sudo&lt;span class="w"&gt; &lt;/span&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/sshtunnel/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;chown -R sshtunnel:sshtunnel ~/sshtunnel/.ssh&lt;/code&gt;: This command recursively sets all files and directories&amp;nbsp;in &lt;code&gt;~/sshtunnel/.ssh&lt;/code&gt; to belong to the sshtunnel user and&amp;nbsp;group.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;chmod ...&lt;/code&gt;: These commands set the permissions of the directory to the minimum necessary. The ssh keys for login are stored in these directories, so it&amp;rsquo;s important to keep them&amp;nbsp;secure.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="on-the-local-server"&gt;On the local&amp;nbsp;server&lt;/h4&gt;
&lt;p&gt;Next, set up the reverse tunnels on the local server to use the newly created&amp;nbsp;user.&lt;/p&gt;
&lt;p&gt;First, create ssh key pair for&amp;nbsp;authentication:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/etc/sshtunnel
sudo&lt;span class="w"&gt; &lt;/span&gt;ssh-keygen&lt;span class="w"&gt; &lt;/span&gt;-qN&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/etc/sshtunnel/id_rsa
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mkdir -p /etc/sshtunnel&lt;/code&gt;: This command creates a new directory for the tunnel credentials to&amp;nbsp;live.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssh-keygen&lt;/code&gt;: This command creates a new key pair for&amp;nbsp;authentication.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-q&lt;/code&gt;: This option silences the output of the ssh-keygen command. It is&amp;nbsp;optional.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-N ""&lt;/code&gt;: This option sets the passphrase of the key pair to an empty string, essentially disabling the password requirement to use it. This makes it easier to allow the automated services to use it without prompting for a password. However, it makes it all the more important to keep these keys private, as anyone can use them to gain access to your&amp;nbsp;system.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-f /etc/sshtunnel/id_rsa&lt;/code&gt;: This option specifies the newly created directory to store the key pair&amp;nbsp;in.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next, modify the systemd service files for the tunnels to use the new user. Most importantly, that means setting these ssh&amp;nbsp;options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-i /etc/sshtunnel/ssh&lt;/code&gt;: This points ssh to the private key we just created for the sshtunnel&amp;nbsp;user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sshtunnel@&amp;lt;remote-server&amp;gt;&lt;/code&gt;: Set the username when connecting to the remote server to&amp;nbsp;sshtunnel.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, copy the public&amp;nbsp;key &lt;code&gt;/etc/sshtunnel/id_rsa.pub&lt;/code&gt; to the remote machine and add it to the authorized keys file&amp;nbsp;in &lt;code&gt;~/sshtunnel/.ssh/authorized_keys&lt;/code&gt;. This will allow the remote server to recognize and authenticate the newly created user. Make sure you connect to the remote server with the new credentials manually from the command line so you can whitelist the remote server&amp;rsquo;s public key as&amp;nbsp;well.&lt;/p&gt;
&lt;h2 id="reverse-proxy"&gt;Reverse&amp;nbsp;Proxy&lt;/h2&gt;
&lt;p&gt;The other component needed is a reverse proxy to direct traffic from the Internet into the private &lt;span class="caps"&gt;SSH&lt;/span&gt; reverse tunnel you set up. The reverse proxy can do a lot of useful things for you, including providing a &lt;span class="caps"&gt;HTTPS&lt;/span&gt; endpoint in front of your services, load balancing, authentication,&amp;nbsp;etc.&lt;/p&gt;
&lt;h3 id="configuring-the-reverse-proxy"&gt;Configuring the reverse&amp;nbsp;proxy&lt;/h3&gt;
&lt;p&gt;I use &lt;span class="caps"&gt;NGINX&lt;/span&gt; for my reverse proxy, but you can really do this part with any web server. The configuration options will be different, but the theory is the&amp;nbsp;same.&lt;/p&gt;
&lt;p&gt;Here is how I have my reverse proxy set up to terminate &lt;span class="caps"&gt;HTTPS&lt;/span&gt; connections and forward through the reverse &lt;span class="caps"&gt;SSH&lt;/span&gt;&amp;nbsp;tunnel:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;server_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;external-domain-name&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;client_max_body_size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;500M&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Only needed if you&amp;#39;re expecting large payloads&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:&amp;lt;reverse-tunnel-remote-port&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Real-IP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_certificate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;path-to-fullchain.pem&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;path-to-privkey.pem&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/etc/letsencrypt/options-ssl-nginx.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_dhparam&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/etc/letsencrypt/ssl-dhparams.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;external-domain-name&amp;gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$host$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;server_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;external-domain-name&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# managed by Certbot&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The second server block just redirects all &lt;span class="caps"&gt;HTTP&lt;/span&gt; requests for this domain to &lt;span class="caps"&gt;HTTPS&lt;/span&gt;. This makes it easier for any clients attempting to connect with &lt;span class="caps"&gt;HTTP&lt;/span&gt; first to be forced to use &lt;span class="caps"&gt;HTTPS&lt;/span&gt;. This block is automatically added when using the LetsEncrypt&amp;nbsp;client.&lt;/p&gt;
&lt;p&gt;The first server block handles all of the actual&amp;nbsp;proxying.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;server_name &amp;lt;external-domain-name&amp;gt;&lt;/code&gt;: This binds this server block to a particular domain. Different server blocks can be bound to different domains and ran simultaneously; &lt;span class="caps"&gt;NGINX&lt;/span&gt; will just check which domain a request is for and use the configs for the associated server block. If you don&amp;rsquo;t have this label, it will use it for all incoming requests, but then you would only be able to have one server block configuration for&amp;nbsp;everything.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;client_max_body_size 500M&lt;/code&gt;: By default &lt;span class="caps"&gt;NGINX&lt;/span&gt; limits incoming request payloads to 1 megabyte. This is fine for most applications, but if you need to set it to be larger (for example, Nextcloud or other backup solutions) you can use this option to set that limit higher. Note that sending excessively large payloads to overwhelm your server is a common denial-of-service attack and can result in exorbitant server expenses, so only use this option if you need it and set it to the lowest value that you actually expect to&amp;nbsp;use.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;location /&lt;/code&gt;: This block specifies the &lt;span class="caps"&gt;URL&lt;/span&gt; root. Setting the root&amp;nbsp;to &lt;code&gt;/&lt;/code&gt; will make this block handle all requests on the domain. You can specify a &lt;span class="caps"&gt;URL&lt;/span&gt; fragment if you want to run multiple services on the same domain. For example, you can have one service in&amp;nbsp;a &lt;code&gt;location /foo&lt;/code&gt; block and another service in&amp;nbsp;a &lt;code&gt;location /bar&lt;/code&gt; block, and then have different settings in each one. Then requests coming in&amp;nbsp;at &lt;code&gt;mydomain.com/foo&lt;/code&gt; will be sent to a different &lt;span class="caps"&gt;SSH&lt;/span&gt; tunnel than requests coming in&amp;nbsp;to &lt;code&gt;mydomain.com/bar&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
While this configuration will allow &lt;span class="caps"&gt;NGINX&lt;/span&gt; to route subpath requests to different &lt;span class="caps"&gt;SSH&lt;/span&gt; tunnels, the applications running behind those tunnels will have to be configued to support such a configuration, as clients will still end the entire &lt;span class="caps"&gt;URL&lt;/span&gt; with the subpath in their &lt;span class="caps"&gt;HTTP&lt;/span&gt; requests. Not all applications support running on a subpath, so it&amp;rsquo;s up to you to decide which ones, if any, will run that way. I personally prefer to use subdomains to separate my services&amp;nbsp;(e.g. &lt;code&gt;foo.mydomain.com&lt;/code&gt; and &lt;code&gt;bar.mydomain.com&lt;/code&gt;) as it&amp;rsquo;s easier and cleaner to support, but you do need full control over your domain to do&amp;nbsp;so.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;proxy_pass http://127.0.0.1:&amp;lt;reverse-tunnel-remote-port&amp;gt;&lt;/code&gt;: This option specifies where traffic in this block should be forwarded to.&amp;nbsp;The &lt;code&gt;127.0.0.1&lt;/code&gt; specifies it should be forwarded to the localhost, where the reverse tunnel is&amp;nbsp;listening. &lt;code&gt;&amp;lt;reverse-tunnel-remote-port&amp;gt;&lt;/code&gt; should be the same remote port you specified when setting up the reverse tunnel. We are connecting the reverse proxy to the tunnel&amp;nbsp;here.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;proxy_set_header ...&lt;/code&gt;: These configs are to properly pass request headers through the proxy to the application behind the proxy. Many services require these headers to be set properly, especially if you&amp;rsquo;re running security applications. Applications may not work, generate security errors, or report incorrect data if these are headers are not set&amp;nbsp;correctly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The remaining headers in the server block are to configure &lt;span class="caps"&gt;HTTPS&lt;/span&gt;. These should be automatically added when requesting a certificate through&amp;nbsp;LetsEncrypt.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="nginx-configuration-files"&gt;&lt;span class="caps"&gt;NGINX&lt;/span&gt; configuration&amp;nbsp;files&lt;/h3&gt;
&lt;p&gt;The configuration for a reverse proxy should be saved with the filename that matches the domain it&amp;rsquo;s running on. For example, if your reverse proxy is running for the&amp;nbsp;domain &lt;code&gt;foobar.mydomain.com&lt;/code&gt;, then you would save the configuration in a file&amp;nbsp;called &lt;code&gt;foobar.mydomain.com&lt;/code&gt;. The directory for these configurations is by&amp;nbsp;default &lt;code&gt;/etc/nginx/sites-available&lt;/code&gt;. Then, when you&amp;rsquo;re finished editing the configuration file, create a symlink to the file&amp;nbsp;in &lt;code&gt;/etc/nginx/sites-enabled&lt;/code&gt; using the command&amp;nbsp;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;target-path&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;link-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The target path is the full path to the file you want to symlink to (for&amp;nbsp;example, &lt;code&gt;/etc/nginx/sites-available/foobar.mydomain.com&lt;/code&gt;) and the link-name is the name you want the link to have (in most cases, it should be the same). When you are done, you can check the link by&amp;nbsp;typing &lt;code&gt;ls -l&lt;/code&gt; and you should see something like&amp;nbsp;this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;lrwxrwxrwx 1 root root 45 Apr  6  2024 foobar.mydomain.com -&amp;gt; /etc/nginx/sites-available/foobar.mydomain.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once the symlink is created, your reverse proxy configuration is now ready to be used. First run the&amp;nbsp;command &lt;code&gt;nginx -t&lt;/code&gt; to test your configuration files. If there are any errors, fix them now. When the test returns successfully, you can reload the &lt;span class="caps"&gt;NGINX&lt;/span&gt; configs with the&amp;nbsp;command &lt;code&gt;systemctl reload nginx&lt;/code&gt;. Your reverse proxy is now live and directing &lt;span class="caps"&gt;HTTP&lt;/span&gt; traffic to your&amp;nbsp;tunnel.&lt;/p&gt;
&lt;h2 id="alternatives-to-self-hosting"&gt;Alternatives to&amp;nbsp;self-hosting&lt;/h2&gt;
&lt;p&gt;If you don&amp;rsquo;t have a publicly accessible server or don&amp;rsquo;t want to purchase a &lt;span class="caps"&gt;VPS&lt;/span&gt; (virtual public server) to host the external reverse proxy, there are other services that will do the equivalent of these steps for you. I have used &lt;a href="https://packetriot.com/"&gt;Packet Riot&lt;/a&gt; in the past to forward traffic from the internet to my local network. Their free tier offers up to one tunnel, unlimited &lt;span class="caps"&gt;HTTP&lt;/span&gt;/s proxies for that tunnel, 1 &lt;span class="caps"&gt;TCP&lt;/span&gt; proxy for the tunnel, and up to 1 gigabyte of bandwidth per month. The next tier is only $5 / month and allows up to a terabyte of bandwidth. I like this service quite a bit, but ultimately had some intermittent issues that led me to pursue hosting my own reverse proxy since I already paid for a &lt;span class="caps"&gt;VPS&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Many self-hosters use ngrok as a reverse proxy for their services. It&amp;rsquo;s quite a bit more expensive but can handle almost everything for you. Tailwind is also a great option if you don&amp;rsquo;t need your services to be truly public, but want to access your services from your own devices from anywhere in the world. There&amp;rsquo;s countless other alternatives out there that will all do the same thing, but ultimately under the hood they function the same way as the tools described in this&amp;nbsp;document.&lt;/p&gt;</content><category term="Technical"/><category term="how-to"/></entry></feed>