<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Baqueano]]></title><description><![CDATA[We are a software and media consultancy! Striving to make and create useful software and great media.]]></description><link>https://baqueano.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6abf943c5090026ac0facb9d/d8db0eb8-0ada-4c54-8ab2-ed55049ea032.png</url><title>Baqueano</title><link>https://baqueano.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 26 Oct 2026 21:28:19 GMT</lastBuildDate><atom:link href="https://baqueano.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a database migration tool from scratch - Part 1]]></title><description><![CDATA[A poor mans commentary on commercial migration tools.]]></description><link>https://baqueano.hashnode.dev/building-a-database-migration-tool-from-scratch-part-1</link><guid isPermaLink="true">https://baqueano.hashnode.dev/building-a-database-migration-tool-from-scratch-part-1</guid><dc:creator><![CDATA[Rodrigo Sanchez]]></dc:creator><pubDate>Sat, 17 Oct 2026 03:39:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/0b707168-bf85-46e1-bd1f-f3edfbd9f19f.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Intro</h2>
<p>Why? Why the hell not!</p>
<p>One of the things that has always fascinated me as a full-stack developer is the database tooling landscape. For some reason, it's always felt a bit archaic to me. Old UIs, new applications that die after a few seasons (looking at you, Azure Data Studio), plugins to let you perform functionality you'd think would be included in the IDE, and don't even get me started on import and export functionality.</p>
<p>Granted, it's probably my fault for always sticking to <code>MSSQL</code> instead of branching out to something shiny and modern like <code>PostgreSQL</code> (it's on the TODO list).</p>
<p>With that said, migrations have always fallen into the category of things I consider fundamental to the lifecycle of a database. Yet a lot of industry projects I've seen tend to follow very similar patterns. Either you use some form of proprietary software that your developers have to learn the ins and outs of, or you roll out your own novel in-house project that is built in a rush and then lost to time, with the original developer leaving and no one knowing how to work with it.</p>
<p>The third option I've seen is to go with a FOSS solution, but that often only lasts a few months before limitations are reached or frustrations arise due to a lack of support for certain features.</p>
<p>Now, I've been a fan of <code>Redgate's Flyway</code> for a while and have always wanted to get my hands on the paid-tier features (schema generation, drift reports, etc.). Unfortunately, it's not in the budget this year... or the next.</p>
<p>But also we need something ASAP because our current tool was written five years ago, needs an update, and we don't have the source code anymore.</p>
<p>So let's build something that will put proprietary software to shame!</p>
<h2>The problem</h2>
<p>What are we actually trying to solve?</p>
<p>At its core the problem seems simple. I have a database in state A and i want it to be in state B, then state C and so on. We need this process to be repeatable and predictable and ideally simple to use. Pretty straight forward, right?</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/ad8140de-c032-4830-b5f0-01d072145d6a.png" alt="" style="display:block;margin:0 auto" />

<p>Well... Specs change, we introduce bugs and sometimes we may incidentally run a script or two directly on a production server to fix urgent and forget about scripting that change out. Oops...</p>
<p>And what about stored procedures, views, functions, triggers and Schema binds? What about all the other objects we can create within SQL? Do we track them too? Or are we going to limit ourselves to just tables? (boring)</p>
<p>And what about data? There's plenty of times where we might need to drop a column, a table, an index or rename things? Should we stop the process? email a developer? require input for confirmation?</p>
<p>What if i want to keep track of a schema over time instead just migrations files. Keeping track of what the current schema is and being able to see it change over time without having to inspect every commit or migration folder would be good.</p>
<p>Where will we run? do we have users? or are we tooling in a CI/CD process? Maybe we need both incase something goes wrong?</p>
<p>What happens if developers create migrations touching the same object? How is it merged and in what order? Can we neatly tie code for a feature to a migration?</p>
<p>Our solution needs to account for all of these possibilities and or eventualities, while making sure the integrity of the database.</p>
<h2>Requirements</h2>
<p>With most of the problem established, lets build our specs and features</p>
<ul>
<li><p>Migration identity: Each migration needs a unique identity and predictable execution order.</p>
</li>
<li><p>Migration tracking: We need to be able to see who or what ran a migration, what scripts did it run, how long it took and ideally what objects might have changed.</p>
</li>
<li><p>Migration atomicity: Each migration should be atomic. It either all succeeds or the whole thing fails.</p>
</li>
<li><p>Migration validation: We need a way to validate the SQL before it gets run, and some rules around what we consider to be a valid migration folder, migration file and schema change.</p>
</li>
<li><p>Controlled execution: We need to be to avoid re-running migrations, select only pending, select by a date range and other selection methods.</p>
</li>
<li><p>Failure handling: If a migration fails, we need useful error information and a clearly defined strategy for handling the resulting database state.</p>
</li>
<li><p>Structured logging: We will need a lot of logging to make sure things are working correctly and so that we can trace why things go wrong.</p>
</li>
<li><p>Configuration: To make some things easier, we should allow users to store connection string, behavioral settings and tweaks.</p>
</li>
<li><p>Schema view: I want to be able to view a pure schema definition and its changes over time without searching every folder. Something like a git history.</p>
</li>
</ul>
<h3>Extra features</h3>
<p>The above is what i would consider the foundation to the tool, this is what most FOSS and community editions essentially cover. Below are some of the more advanced features id like to get to:</p>
<ul>
<li><p>Schema drift detection: Compare two schemas and find differences.</p>
</li>
<li><p>Schema script generation: Automatic migration file generation for comparisons or automatic flow.</p>
</li>
<li><p>Rollback or recovery flow: Compare schema snapshots and generate a possible path back to an earlier state. This would be a recovery aid, not a guarantee of a safe rollback.</p>
</li>
<li><p>UI: for developer flow integration</p>
</li>
</ul>
<p>Some of these might turn out to be more complicated than they're worth, while others might become essential once we start using the tool in practice. I know I've probably missed some things too.</p>
<p>For now, the priority is getting a reliable migration engine working before worrying about all the shiny extras.</p>
<h2>The libraries</h2>
<p>I spent a bit of time doing research and found some useful libraries. Lucky for us Microsoft already provides much of the SQL Server tooling we need, so I don't have to write a parser or build a schema-comparison system from scratch. I have chosen to avoid this, you however may want to own the whole stack so i will list them below so you can look them up and see the complexity of these projects yourself.</p>
<h3>The heavy lifters</h3>
<ul>
<li><p><code>Microsoft.SqlServer.DacFx</code>: the same engine behind SSDT and <code>SqlPackage</code>. A <code>.sqlproj</code> builds into a dacpac, which is a complete model of the database. DacFx can compare two dacpacs, or a dacpac against a live database, and spit out the script to get from one to the other. This is the library that gets us the "paid-tier" features: schema generation, drift reports and deploys.</p>
</li>
<li><p><code>Microsoft.SqlServer.TransactSql.ScriptDom</code>: Microsoft's <code>T-SQL</code> parser. It turns a script into a syntax tree you can walk with a visitor. We use it to validate SQL before it runs, work out which object a script touches, split generated scripts into one file per object, and safely rewrite scripts on the fly.</p>
</li>
<li><p><code>git</code>: not a library, but it's doing a lot of the work. The schema project lives in source control anyway, so instead of storing snapshots ourselves we let git be the schema history.</p>
</li>
<li><p><code>git archive</code> pulls the schema project out at any commit without touching your working tree, and <code>git ls-tree</code> tells us which migration folders existed at a given commit. That's what lets us generate a migration (or a rollback) by comparing two commits, no database needed.</p>
</li>
</ul>
<h3>Talking to the database</h3>
<ul>
<li><p><code>Microsoft.Data.SqlClient</code>: the official SQL Server driver.</p>
</li>
<li><p><code>Dapper</code>: a thin layer over ADO.NET for the journal queries. No ORM, no magic, just SQL I can read.</p>
</li>
</ul>
<h3>The CLI</h3>
<ul>
<li><p><code>Spectre.Console.Cli</code>: command parsing, help text, and the pretty tables, spinners and colours in the terminal.</p>
</li>
<li><p><code>Serilog</code>: structured logging to the console and to rolling log files, so we can trace why things went wrong after the fact.</p>
</li>
<li><p><code>Microsoft.Extensions.*</code>: dependency injection and layered JSON/environment configuration, the same stuff you'd use in an ASP.NET app.</p>
</li>
</ul>
<h3>Testing and the IDE</h3>
<ul>
<li><p><code>xUnit v3</code>: regression tests over Core.</p>
</li>
<li><p><code>Microsoft.VisualStudio.Extensibility</code>: the new out-of-process VS extension model, used for the tool window.</p>
</li>
</ul>
<h2>Project structure</h2>
<p>Ill be using <code>C#</code> for this project. I wanted to use <code>C++</code> or <code>Rust</code> but i couldn't find equivalent <code>SQL</code> graphing or parsing libraries or bindings.</p>
<p>Below is going to be my initial project structure:</p>
<ul>
<li><p>MigDb.Core: the main library that can implemented by whatever fronted we want to wrap it around.</p>
</li>
<li><p>MigDb.CLI: so that it can be invoked without blocking in CI/CD.</p>
</li>
<li><p>MigDb.Tests: for regression testing</p>
</li>
<li><p>MigDb.VSExtension: This is a choice i know.</p>
</li>
</ul>
<p>I want to explain the choice of using VS Extension before i go further. Our previous workflow used Git Hooks more precisely pre-commit hooks. You'd finish your commit and every single time regardless of whether you had database changes or not a form would be triggered where you were asked if database changes were made and you had to respond and fill out if you did.</p>
<p>I wanted to change the workflow to be more passive. If i knew i had database changes i could just click a button on my IDE and i would invoke it. I feel the responsibility should be on the developer to seek the tool instead of it being shoved in their face. So this little extension will be a window that can be pinned to the side or moved around until you need it.</p>
<pre><code class="language-plaintext">MigDb
├── MigDb.CLI/
├── MigDb.Core/
├── MigDb.Test/
└── MigDb.VSExtension/
</code></pre>
<h2>Conclusion</h2>
<p>So that's the plan. We have a problem that looks simple until you start poking at it, a list of requirements that's probably longer than it should be, and a set of libraries that means we don't have to reinvent the hard parts. DacFx and ScriptDom do the heavy lifting on the SQL side, and git gives us the schema history for free.</p>
<p>The goal isn't to clone Flyway feature for feature. It's to build something small enough that the next developer can pick it up and understand it, but solid enough that we trust it with production databases.</p>
<p>In the next part we will dive deeper into our migration strategy and possible folder structures and how we can solve some of the problem questions.</p>
<p>If youve made it this far, thank you for taking the time to read this and ill catch you in the next one!</p>
]]></content:encoded></item><item><title><![CDATA[Securely sharing media from OpenMediaVault with FileBrowser Quantum and Cloudflare Tunnel]]></title><description><![CDATA[A secure way to create expiring and password protected share links]]></description><link>https://baqueano.hashnode.dev/securely-sharing-media-from-openmediavault-with-filebrowser-quantum-and-cloudflare-tunnel</link><guid isPermaLink="true">https://baqueano.hashnode.dev/securely-sharing-media-from-openmediavault-with-filebrowser-quantum-and-cloudflare-tunnel</guid><category><![CDATA[#selfhosted]]></category><category><![CDATA[Homelab]]></category><category><![CDATA[omv]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[#filebrowser]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[nas]]></category><dc:creator><![CDATA[Rodrigo Sanchez]]></dc:creator><pubDate>Sun, 11 Oct 2026 13:23:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/74213e68-a2f6-4bc6-b295-d7402288b709.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<h2>Context</h2>
<p>Recently I've had the opportunity to work on a few long-form media projects. These projects required us to collect what I would consider a fair bit of media for what are relatively small theatre companies that wanted to try their hand at increasing their social media exposure for shows. To give you an idea, one project required multiple photo shoots, which resulted in hundreds of photos, and then required full recordings of a tech run and a final recording of the full show on closing night. The stills and video needed to be processed like normal, but the videos needed to be taken further to create trailers and social media content, and also cut into clips so that actors could show off their performances in their portfolios.</p>
<p>To give you a sense of how the data scaled, a single 3-hour show run in 4K ProRes 422 HQ comes to roughly 800GB. Multiply this by two because we have to do closing night too, and we have ~1.6TB of footage. The 600 Fuji RAW files then add another ~10GB.</p>
<p>After a few days of editing we can shrink this down to a few ~25GB renders, each with different colour grades and feels, and about 4GB of stills. This is great, that's a lot of space saved. Final project files end up being ~80GB give or take. Oh, and those renders are H.265. Yes, I know AV1 exists, but we have to stay compatible with client hardware. And nobody is building new PCs with the current price of hardware.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/5ede43c8-448d-4a03-b91a-441a97a9a0a2.png" alt="" style="display:block;margin:0 auto" />

<h2>The problem</h2>
<p>Typically my projects don't require 3 hours of straight recording, so I've been able to get away with juggling cloud services. If the final render is under 15GB I can just use Google Drive and let Google handle security and links. If not, maybe we need to split project files and put the stills on Dropbox to make it all work.</p>
<p>But free tiers only go so far, and growing them 500MB at a time with referrals isn't a plan. And what happens when a client asks for the raw footage?</p>
<p>If you've made it this far, thank you, and you can probably guess where this is going: how do I get these files to clients without mailing or lending out my 2TB NVMe?</p>
<p>The obvious answer is yet another monthly subscription for 1–2TB of cloud storage. The <em>responsible</em> answer might be Azure Storage, handing out SAS URLs to blobs, and wrapping everything in password-protected zips.</p>
<p>But that's boring, and I've got a Dell R730xd under my desk that's spent far too long as a footrest.</p>
<h2>Part 1 - The setup</h2>
<p>The server I received already came with ESXi installed and a few TBs that are managed through an internal RAID controller. I won't go into too much detail about the server itself, since the same result can be achieved on bare metal, a VM or containers. The main requirement is a place you can install OpenMediaVault (OMV) and a big storage pool. I was lucky enough that the server came with an HDD1 store of 12TB, and I've added HDD2 and HDD3 as supplementary storage for apps, config backups and other things.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/497795d5-1203-499a-9e7c-7bf5748e548b.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/dbbd739a-e3b8-46b4-b3d7-1e8ecd3444bd.png" alt="" style="display:block;margin:0 auto" />

<p>I've chosen to separate the data by using HDD1 as the main storage location for FileBrowser Quantum and HDD2 for Docker components and any other configs, because I prefer the physical boundary. You don't have to do it this way. You may well only have one giant pool; you'll still be able to separate the locations using OMV's shared folder functionality.</p>
<h3>Installing the OS</h3>
<hr />
<p>OMV has a lot of useful documentation, and the installation process is pretty straightforward with their installer. You burn the ISO to a USB and boot into the installer. If you're on Windows, Rufus is the tool I use; otherwise Etcher or <code>dd</code> can be used on Linux.</p>
<p>Through ESXi we can skip all that and upload the ISO image directly to one of our datastores, so that we can bind it to the VM during the creation step. We then run through the VM creation steps, choosing Linux as the guest OS family, Debian as the OS version, and binding the ISO.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/0cf05b68-b97c-47bf-bb7b-d6d92c04243e.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>Another great tweak is to enable the VM to start on boot, so if the server restarts or something happens the VM automagically comes back online.</p>
</blockquote>
<p>You should now be able to follow the standard installation guide from OMV and end up with a similar-looking terminal displaying an IP that hosts the web frontend, and some credentials to use on the login page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/4cfa834a-5968-4046-b70d-5f8c580038c5.png" alt="" style="display:block;margin:0 auto" />

<p>Install done.</p>
<h3>Configuring OpenMediaVault</h3>
<hr />
<p>At this stage you should be able to log in to OMV through your browser and click around to familiarise yourself with the interface. I would also strongly recommend you check for updates under</p>
<p><code>System -&gt; Update Management -&gt; Updates</code></p>
<p>so that you have the latest security fixes and features. Oh, and make sure all your disks show up under</p>
<p><code>Storage -&gt; Disks</code></p>
<p>We should now be able to start creating Shared Folders and set up SMB. I will create three Shared Folders:</p>
<ol>
<li><p>DockerAppData - Where we store our Docker compose files</p>
</li>
<li><p>FileBrowserShares - Where we store what we want to share</p>
</li>
<li><p>Media1 - The original location of all our data (optional)</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/b10595f8-67e0-4809-a3ef-a988712a3eb0.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>I have Media1 here because I was already using OMV for storing the giant RAW footage. Everything can work off a single share, but then all of it will be exposed to the container.</p>
</blockquote>
<p>Now we just need to enable a way to move files over the network to OMV. Personally I picked SMB for this, since NFS permissions are IP-based and file ownership uses user IDs. Rsync is also a really good option for large files.</p>
<p>Head down to</p>
<p><code>Services -&gt; SMB/CIFS -&gt; Settings</code></p>
<p>and enable it there. You should now be able to create SMB shares under</p>
<p><code>Services -&gt; SMB/CIFS -&gt; Shares</code></p>
<p>Another optional step is to create a user or a group if you want to limit actions. You should only need one or two shares depending on your setup. Point them at the correct Shared Folders from the previous step:</p>
<ol>
<li><p>FileBrowserShares - Where we drop our final render</p>
</li>
<li><p>Media1 - Where we store the raw footage</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/b097274a-479c-447e-9571-f3e09a2c8d42.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/974cc018-3558-49b1-a8d7-6d13f8d08691.png" alt="" style="display:block;margin:0 auto" />

<p>Add it to your file browser of choice or your <code>fstab</code> file and test.</p>
<h3>Setting up OMV Docker</h3>
<hr />
<p>With OMV Shared Folders done and SMB set up, we now need to set up Docker and omv-extras, a set of community-maintained packages that add extra functionality.</p>
<p>In a terminal, either directly or through SSH, we need to run the install script.</p>
<pre><code class="language-shell">wget -O - https://github.com/OpenMediaVault-Plugin-Developers/packages/raw/master/install | bash
</code></pre>
<p>You should now have a new option in your sidebar under</p>
<p><code>System -&gt; omv-extras</code></p>
<p>Tick the Docker repo box and click the big blue Save button. Then run apt clean and wait until it says done.</p>
<p>With omv-extras enabled we should now be able to install openmediavault-compose, which will let us create our Docker containers from compose files. Install the plugin under <code>System -&gt; Plugins</code> and refresh the page. If you get a connection lost message, it's because OMV restarts its web UI server; just refresh the browser.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/b39d756f-21ae-44b8-9788-4be9b18cc08e.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/eb4996b3-a979-4f3d-97b4-17300d316e4c.png" alt="" style="display:block;margin:0 auto" />

<p>In your sidebar you should now see a new</p>
<p><code>Services -&gt; Compose -&gt; Settings</code></p>
<p>page, which we will need to set up. The first thing you'll want to change is your compose files shared folder. Point this to your DockerAppData shared folder and hit Save. Then hit Install or Reinstall Docker.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/4b3ac841-6e7e-4f04-a4ec-fd4589961690.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>DON'T FORGET TO HIT INSTALL OR REINSTALL DOCKER AT THE BOTTOM</p>
</blockquote>
<h2>Part 2 - Installing FileBrowser and cloudflared</h2>
<p>At this point you should have OMV installed with SSH or direct terminal access, a few Shared Folders set up, SMB or some other way to sync files over the network, and omv-extras with Docker enabled and configured for compose files.</p>
<p>One hard requirement for this second part is a domain using Cloudflare DNS (the free plan is fine), which gives you access to Cloudflare Zero Trust tunnels. With that said, we're going to start this section by getting the tunnel set up before continuing with OMV. So go ahead and open a new tab or browser window.</p>
<p>We're going to start by creating a tunnel through the Cloudflare dashboard. You should be able to find the option under</p>
<p><code>Protect and Connect -&gt; Zero Trust</code></p>
<p>This should redirect you to a new page with a different nav bar that should have</p>
<p><code>Networks -&gt; Tunnels</code></p>
<p>Here you should be able to create a new tunnel of type <code>Cloudflared</code>, then for your connector make sure you select Docker.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/e3409911-6f6c-4462-ac2e-ae98558b337e.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>IMPORTANT: Save the token displayed on this page, as it will be used in our compose file later.</p>
</blockquote>
<p>Finally, at the publish step you should be able to set a subdomain and a path. I've chosen <code>files</code> as my subdomain, so I get <code>files.[my-domain].com</code>.</p>
<p>The other crucial step here, if you want to narrow access to just the generated links, is to set the Path field to <code>^/public/.*</code>. This stops intruders from spamming your login page and scraping anything that's outside of FileBrowser's public folder. The tradeoff is that you also won't be able to access the login page or application externally. For me this isn't an issue, as 90% of the time I will be accessing it locally.</p>
<p>Lastly, the service section is the connection between our <code>cloudflared</code> container and <code>FileBrowser Quantum</code> container. We want to set the type to <code>HTTP</code> because FileBrowser serves plain HTTP and this hop never leaves the VM. Visitors still get HTTPS via Cloudflare, and the tunnel itself is encrypted. Set the URL to <code>filebrowser:80</code> (we can use the container names here since Docker provides name resolution between containers; alternatively you could use <code>[server-ip]:8080</code>).</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/71dba02d-c0fb-4883-b1bb-9a7c815dac5a.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/1610897f-6909-414b-932d-ff2281b4a59b.png" alt="" style="display:block;margin:0 auto" />

<p>With the tunnel all set up, we can go back to OMV and start writing our compose file!</p>
<h3>Final steps - Back to OMV</h3>
<hr />
<p>OK, we're nearly done. Hopefully you haven't run into any issues! With the tunnel all set up, we can write our configs and compose files and try to spin up our containers!</p>
<p>For this step, make sure you have that tunnel connector token handy.</p>
<p>First we're going to create the FileBrowser config file, so that when the container spins up it's ready to go. This typically lives under a <code>/data</code> folder in FileBrowser. We're also going to create a separate volume in our container called <code>/deliveries</code> that will be the publicly exposed directory.</p>
<p>Create a new folder under <code>DockerAppData/filebrowser/data</code>:</p>
<pre><code class="language-shell">mkdir -p "/srv/dev-disk-by-uuid-[disk-uuid]/DockerAppData/filebrowser/data"
</code></pre>
<p>Now create a new <code>config.yaml</code> file inside this folder using a text editor such as <code>nano</code> or <code>vim</code>. Make sure you set your external URL correctly.</p>
<pre><code class="language-yaml">http:
  externalUrl: "https://files.[domain].com"
  trustProxyHeaders: true
  disableWebDAV: true

server:
  cacheDir: /home/filebrowser/data/tmp
  sources:
    - path: /deliveries
      name: Deliveries
      config:
        defaultEnabled: true

frontend:
  name: "MySiteName"
  description: "Client video delivery"
</code></pre>
<blockquote>
<p>The path here needs to match the compose file's volume from our next step, so take note.</p>
</blockquote>
<p>Lastly, we need to set the owner to the container's user so it can read and write to its database.</p>
<pre><code class="language-shell">chown -R 1000:1000 "/srv/dev-disk-by-uuid-[disk-uuid]/DockerAppData/filebrowser/data"
</code></pre>
<p>Now for our compose file. Back in OMV's web UI we can create compose files directly by navigating to</p>
<p><code>Services -&gt; Compose -&gt; Files</code></p>
<p>You should get a form where you can copy and paste the following YAML, substituting your disk UUIDs and your token from Cloudflare's tunnel setup.</p>
<pre><code class="language-yaml">services:
  filebrowser:
    image: ghcr.io/gtsteffaniak/filebrowser:2.0-stable
    container_name: filebrowser
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - /srv/dev-disk-by-uuid-[disk-uuid]/FileBrowserShares:/deliveries:ro
      - /srv/dev-disk-by-uuid-[disk-uuid]/DockerAppData/filebrowser/data:/home/filebrowser/data

  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run --token [MyBigToken]
    depends_on:
      - filebrowser
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/d7f69098-7492-4b2a-8623-3a83dd2c3edc.png" alt="" style="display:block;margin:0 auto" />

<p>You'll notice here that we mount <code>/deliveries</code> and <code>/data</code> from our previous step. The rest of the YAML is pretty self-explanatory. Hit that Save button.</p>
<blockquote>
<p>The <code>:ro</code> at the end of the <code>/deliveries</code> volume mounts it read-only. FileBrowser can show and share your files, but it can never modify or delete them, even if something goes wrong. I've done this because I'm testing and didn't want to accidentally delete files. Once you're happy you can remove this</p>
</blockquote>
<p>Back in the main list you should be able to see a new row containing the new FileBrowser compose file. Select the row and click the "Up" button in the toolbar. This will download the images and spin up our containers!</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/66cf1416-8fbf-49a8-a21d-1e7997db6e8e.png" alt="" style="display:block;margin:0 auto" />

<h2>Part 3 - The fun part!</h2>
<hr />
<p>If everything went well, you should see a green "Up" status and the bound ports on the right. You can click the URLs or navigate to your server's IP on port 8080 and see a login screen!</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/caf83b92-7625-4376-9b2d-e54e779310fa.png" alt="" style="display:block;margin:0 auto" />

<p>You'll need to grab the credentials from the Docker logs, as they are auto-generated by default. You can do this in the terminal using:</p>
<pre><code class="language-shell">docker logs filebrowser 2&gt;&amp;1 | grep -i password
</code></pre>
<p>Log in as <code>admin</code> using that password (you'll be asked to set a new one straight away), and you should be greeted with an empty dashboard! You can now prep some files and drag them through SMB, or whatever network method you chose before, into your <code>FileBrowserShares</code> shared folder and they will appear!</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/7c522434-23e0-4131-a4fc-75ed2f91cfd1.png" alt="" style="display:block;margin:0 auto" />

<p>Back in Cloudflare Tunnels you should now be able to see a Healthy status and one active connector, which confirms everything is hooked up correctly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abf943c5090026ac0facb9d/70fa3c34-4d01-46a9-a88c-da18fc2ebf31.png" alt="" style="display:block;margin:0 auto" />

<h3>Testing Links</h3>
<hr />
<p>The final step is to right-click a directory or file and hit Share to generate a link! Give it an expiration time and a password and test it internally. You can then send it to someone, or test it on your phone, to make sure it works externally.</p>
<p>You should also test that hitting <code>files.[domain].com</code> directly results in a 404, as we don't want direct traffic.</p>
<p>If you've reached the end, everything should be working, and your FileBrowser instance should be tunnelled and only allow publicly served content and links in and out.</p>
<h2>Conclusion</h2>
<p>This was quite a journey, I'm not going to lie. It was my first time working with Cloudflare and its many features, and frankly I'm not convinced it's the absolute best solution to my problem. Maybe blob storage in a cloud provider would have been way easier, who knows. But it was an interesting mini project that taught me a lot, and I'll be testing it out for a few months.</p>
<p>Ive heard Cloudflare could potentially throttle this solution since serving large video is restricted and has to be routed through their paid services like Stream or R2 but the writing on the wall is that it only applies to 'disproportionate' usage. it will be interesting to see if this applies to my case of a few files a month.</p>
<p>If anybody knows of any other similar solutions, let me know! I'd love to see how other people have solved this.</p>
<hr />
<p>References</p>
<ul>
<li><p><a href="https://www.openmediavault.org/install.html">https://www.openmediavault.org/install.html</a></p>
</li>
<li><p><a href="https://docs.openmediavault.org/en/stable/installation/index.html">https://docs.openmediavault.org/en/stable/installation/index.html</a></p>
</li>
<li><p><a href="https://github.com/gtsteffaniak/filebrowser">https://github.com/gtsteffaniak/filebrowser</a></p>
</li>
<li><p><a href="https://forum.openmediavault.org/index.php?thread/5549-omv-extras-org-plugin/">https://forum.openmediavault.org/index.php?thread/5549-omv-extras-org-plugin/</a></p>
</li>
<li><p><a href="https://www.cloudflare.com/service-specific-terms-application-services/">https://www.cloudflare.com/service-specific-terms-application-services/</a></p>
</li>
<li><p><a href="https://blog.cloudflare.com/updated-tos/">https://blog.cloudflare.com/updated-tos/</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>