<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Noah Masur's Site</title><link>https://nmasur.github.io/</link><description>Recent content on Noah Masur's Site</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 26 Nov 2024 16:28:36 -0700</lastBuildDate><atom:link href="https://nmasur.github.io/index.xml" rel="self" type="application/rss+xml"/><item><title>Don't Bring Your Work MacBook To The Apple Store</title><link>https://nmasur.github.io/posts/corporate-macbook-apple-store/</link><pubDate>Tue, 26 Nov 2024 16:28:36 -0700</pubDate><guid>https://nmasur.github.io/posts/corporate-macbook-apple-store/</guid><description>&lt;p>It all started when I plugged in my MagSafe charging cable and saw that it was
blinking orange. I looked at my Mac and noticed it was not charging at all.
Rebooting didn&amp;rsquo;t help, or using a different charging cable.&lt;/p>
&lt;p>So I decided to go to the Apple Store. I could have brought it to the IT
department at work instead, but then I would have to setup a loaner machine
while I waited for the repair. I was working remotely out of state anyway;
plus, since it was just a broken charging port, I figured it would be an easy
fix. Right?&lt;/p>
&lt;p>Right?&lt;/p>
&lt;h1 id="first-visit-bad-omens">First Visit: Bad Omens&lt;/h1>
&lt;p>The first warning sign of trouble that I should have noticed was when I brought
in the machine: they said they needed to restart it to run a diagnostic, and
they asked me for the firmware password to unlock the bootloader. I told them
that the firmware password was controlled by JAMF, and I would need to ask
someone at the office to send me the latest version of the password (it was
outside of office hours).&lt;/p>
&lt;p>The Apple rep told me that they could order the part for me anyway so it&amp;rsquo;s
ready when I came back in. I shrugged but I knew that would be heading back
home by the time the part arrived. I could still charge my computer with a
USB-C cable, so I put it off until later.&lt;/p>
&lt;h1 id="second-visit-the-monster-awakens">Second Visit: The Monster Awakens&lt;/h1>
&lt;p>I spoke to the company IT department and asked how I can get the JAMF password.
They said that it rotates every 15 minutes, so someone would need to be on hand
to respond to my request for it during my visit with Apple.&lt;/p>
&lt;p>I decided to wait until a Friday afternoon to go to the store because I could
drop it off for the weekend and pick it up after a day or two without losing
the ability to work. I made the mistake of choosing the Apple Store that was 25
minutes away because I thought I would only have to be there twice.&lt;/p>
&lt;p>When I brought the machine, they were able to run the diagnostic using the
password I got from my colleague. Of course, like the previous store, they had
to order the new charging port so I would have to bring my device back in a few
days.&lt;/p>
&lt;h1 id="third-visit-the-horror-grows">Third Visit: The Horror Grows&lt;/h1>
&lt;p>I dropped off my laptop at the Apple Store and make my way back home, satisfied
that this slightly annoying problem will be alleviated.&lt;/p>
&lt;p>However, the next day I receive a strange email from the Apple Store stating
the following:&lt;/p>
&lt;blockquote>
&lt;p>We have some important information about the service of your product. Please
get in touch as soon as possible—you can contact us at (123) 456-7890.&lt;/p>
&lt;/blockquote>
&lt;p>That seemed a little less than ideal, but I wasn&amp;rsquo;t too worried. I called the
number and found an automated message directing me to various lines. I
mentioned that I was returning a call from the Genius Bar and I was transferred
to another line.&lt;/p>
&lt;p>The line rang for 5 minutes straight and then hung up on me. There was no hold
music, just ringing. Then click and the call was over.&lt;/p>
&lt;p>Confused, I called the number again. Directed to the repair department. Same
response. A third time, after 20 minutes of trying. No difference.&lt;/p>
&lt;p>I called the automated number again and this time asked for the Sales
department. I was immediately sent to a real person, although the Sales
department was apparently centrally located and not connected to my particular
Apple Store. I explained to the Sales rep what happened and how I was trying to
call back the Genius Bar at a particular store location, and they very kindly
helped connect me to someone over there via a direct line of some sort. I was
very grateful as this was not in their job description but they chose to help
me out anyway.&lt;/p>
&lt;p>Getting in touch with the Genius Bar, what did I find out? Yup, they needed to
run a final diagnostic to hand over the computer and therefore they needed the
JAMF password again. Of course.&lt;/p>
&lt;h1 id="fourth-visit-a-creeping-sensation">Fourth Visit: A Creeping Sensation&lt;/h1>
&lt;p>I spend another 25 minutes going to the Apple Store on Monday morning, ready to
pick up my computer. I had informed the IT team that I would need the password
again so I just had to wait for work hours to begin.&lt;/p>
&lt;p>I got the password and had them reboot the machine to run the diagnostic. They
plugged in the charging cable and I noticed it was still blinking orange like
it had before. That did not seem like a good sign. Did the repair technicians
fail to even attempt to charge the device after replacing the cable?&lt;/p>
&lt;p>The diagnostic did not pass. The technician told me that the real issue was
almost certainly with the logic board, whose contact point with the charging
port may have shorted out or something.&lt;/p>
&lt;p>Well, great.&lt;/p>
&lt;p>They said they would order the new logic board, and I would just have to come
in again. They said that the NVME drive is soldered to the board and will be
replaced as well, meaning I need to make sure I backup everything off the
device.&lt;/p>
&lt;p>So I thanked them and left to head into work.&lt;/p>
&lt;h1 id="fifth-visit-something-lurking-just-ahead">Fifth Visit: Something Lurking Just Ahead&lt;/h1>
&lt;p>I wait for another Friday afternoon, after preparing my backups, and leave the
laptop for repair once again. I was ready to have this over with.&lt;/p>
&lt;p>On Saturday, I once again receive the dreaded email:&lt;/p>
&lt;blockquote>
&lt;p>We have some important information about the service of your product. Please
get in touch as soon as possible—you can contact us at (098) 765-4321.&lt;/p>
&lt;/blockquote>
&lt;p>This time I know what to expect. I don&amp;rsquo;t bother calling. I just have to come in
during work hours and get the password again to run the diagnostic once more.
So I wait patiently.&lt;/p>
&lt;h1 id="sixth-visit-absolute-terror">Sixth Visit: Absolute Terror&lt;/h1>
&lt;p>Another Monday morning and another 25 minutes travel to the Apple Store for my
ritual encounter with the repair team.&lt;/p>
&lt;p>However, this time they did not run the diagnostic. Instead, they told me
something unusual. Apparently, according to the repair technician who was
working on the board, they are not capable of replacing a logic board that is
protected by a firmware password.&lt;/p>
&lt;p>They said that the firmware password would need to be removed, which means that
it will need to be essentially unrenrolled from JAMF.&lt;/p>
&lt;p>This was a non-starter.&lt;/p>
&lt;p>Somehow neither the technician who ran the diagnostic and ordered the part nor
the technician who accepted my dropped off machine had understood that this was
the case.&lt;/p>
&lt;p>I knew immediately at that point that I had to give up with the Apple Store.
This whole mess probably should have gone through the IT RMA process the entire
time and I was hopelessly naive.&lt;/p>
&lt;hr>
&lt;p>So the lesson is thus: Don&amp;rsquo;t do what I did. Don&amp;rsquo;t try to bring a managed
computer to the Apple Store for repair.&lt;/p></description></item><item><title>Nix Turns Installing Software On Its Head</title><link>https://nmasur.github.io/posts/nix-install-software/</link><pubDate>Thu, 21 Nov 2024 17:04:18 -0700</pubDate><guid>https://nmasur.github.io/posts/nix-install-software/</guid><description>&lt;p>There are plenty of benefits to using the &lt;a href="https://nixos.org/" target="_blank" rel="noopener">Nix package
manager&lt;/a>, NixOS, and the rest of the Nix ecosystem.
However, for people unfamiliar with Nix, one of the fundamental paradigm shifts
is how Nix changes what it means to &lt;em>install&lt;/em> a software package.&lt;/p>
&lt;h1 id="traditional-package-management">Traditional Package Management&lt;/h1>
&lt;p>A traditional package manager will install software programs at the user&amp;rsquo;s whim
to a designated location on disk. These files are automatically exposed to your
environment via a &lt;code>PATH&lt;/code> or simply placed in a known location to be consumed.&lt;/p>
&lt;p>&lt;code>/usr/bin/python3&lt;/code>&lt;/p>
&lt;p>This leads to three problems:&lt;/p>
&lt;ol>
&lt;li>There is no coupling between the environment and the package manager. So if
a package is required by an environment or another package dependency, you
simply have to hope that you already installed it. Now you have an environment
that cannot guarantee that all of its dependencies are available.&lt;/li>
&lt;li>Since you cannot link a package to its dependant environments, you can never
be sure if it can be safely removed from your system. What happens if you
uninstall a package that is required by another program or environment? To
avoid the risk of breaking things, you usually end up with a growing pile of
bloat on your system.&lt;/li>
&lt;li>For software placed in a standard location, you cannot install more than one
version of the same package. Not only does this make managing multiple
environments painful, but sometimes two or more packages might be incompatible
if they both depend on different versions of the same libraries.&lt;/li>
&lt;/ol>
&lt;h1 id="nix-architecture">Nix Architecture&lt;/h1>
&lt;p>These problems are moot with the Nix architecture. This is because Nix doesn&amp;rsquo;t
really have the concept of &amp;ldquo;installing&amp;rdquo; a program. Instead, packages are built
(or downloaded from a cache) and placed in the Nix store, and packages are also
exposed to the environment.&lt;/p>
&lt;h2 id="the-nix-store">The Nix Store&lt;/h2>
&lt;p>The Nix store is simply a location on your system (&lt;code>/nix/store/&lt;/code>) where
software that is built or downloaded by Nix resides. This is an immutable file
system that can only be altered by the Nix daemon. Every package sits in a
specific build directory with a unique name based on its package name and a
hash (such as &lt;code>pwvfrkynkyhbnidikifjz5skxq892g5a-ffmpeg-headless-6.1.1-lib/&lt;/code>).&lt;/p>
&lt;p>These programs can all be accessed with the full Nix store path, but doing so
would be incredibly tedious and impractical.&lt;/p>
&lt;h2 id="nix-environments">Nix Environments&lt;/h2>
&lt;p>Every tool in the Nix ecosystem has a way of exposing packages from the Nix
store to the current environment, such as
&lt;a href="https://nixos.org/manual/nixos/stable/#sec-package-management" target="_blank" rel="noopener">NixOS&lt;/a>,
&lt;a href="https://nix-community.github.io/home-manager/index.xhtml#ch-introduction" target="_blank" rel="noopener">Home-Manager&lt;/a>,
&lt;a href="https://nix.dev/manual/nix/2.17/package-management/profiles" target="_blank" rel="noopener">Nix profiles&lt;/a>, or
&lt;a href="https://nixos.wiki/wiki/Development_environment_with_nix-shell" target="_blank" rel="noopener">Nix shells&lt;/a>.&lt;/p>
&lt;p>With these tools, you use the Nix language to declare the environment&amp;rsquo;s or
program&amp;rsquo;s dependencies, and Nix will automatically resolve them to the path in
the Nix store and use them as shared libraries or place them (or symlink them)
onto your current &lt;code>PATH&lt;/code> to be used like normal programs.&lt;/p>
&lt;h1 id="the-cool-part">The Cool Part&lt;/h1>
&lt;p>Here is the cool part: if you declare a program as a depencency for your
environment, your system, or another program, Nix automatically build (or
download) any dependencies that are missing from the Nix store.&lt;/p>
&lt;p>This solves &lt;strong>problem 1&lt;/strong>: you have a guarantee (after build time) that your
environment includes the dependencies you have declared. What about problems 2
and 3?&lt;/p>
&lt;p>For &lt;strong>problem 2&lt;/strong>, Nix doesn&amp;rsquo;t worry about always keeping the exact set of packages
built in the Nix store. Nix keeps track of which dependencies are required by
your different environments, and when you choose to garbage collect it will
simply purge the existing packages that are not declared as dependencies of
anything else. You can set the garbage collector to run on a schedule; if you
accidentally purge too aggressively, the worst case scenario is that you have
to wait for Nix to rebuild or redownload a few packages.&lt;/p>
&lt;p>For &lt;strong>problem 3&lt;/strong>, the uniqueness of the Nix store paths allow you to reference
multiple packages with the same name. You can expose one version of a package
to one project shell environment and another version to a specific dependant
package, while another lives in your default shell. If they all happen to
require the exact same version, then great &amp;mdash; you get to save some space. But
otherwise, you don&amp;rsquo;t ever have to think about it.&lt;/p>
&lt;h1 id="fundamental-benefits">Fundamental Benefits&lt;/h1>
&lt;p>The Nix ecosystem has a ton of benefits: declarative configuration, build
guarantees, Nixpkgs availability, rollbacks, lazy functional modules, temporary
environments, but fundamentally they all are sourced from rethinking, from the
ground up, what software installation means.&lt;/p></description></item><item><title>Package Language Servers Into Neovim Using Nix</title><link>https://nmasur.github.io/posts/package-neovim-lsp/</link><pubDate>Sat, 16 Nov 2024 11:25:05 -0700</pubDate><guid>https://nmasur.github.io/posts/package-neovim-lsp/</guid><description>&lt;p>The introduction of the &lt;a href="https://microsoft.github.io/language-server-protocol/" target="_blank" rel="noopener">language server
protocol&lt;/a> has made it
easier to use a variety of different text editors for programming with hints
and autocompletion instead of requiring more complex IDEs.&lt;/p>
&lt;p>Unfortunately, it also means that most text editors don&amp;rsquo;t come bundled with
their own language server and instead expect it to be installed separately on
your machine. While there are some tools that intend to make it easier to
install language servers from inside Neovim, such as
&lt;a href="https://github.com/williamboman/mason.nvim" target="_blank" rel="noopener">mason.nvim&lt;/a> or
&lt;a href="https://github.com/dundalek/lazy-lsp.nvim?tab=readme-ov-file" target="_blank" rel="noopener">lazy-lsp.nvim&lt;/a>,
I prefer the guarantee that the Neovim package contains everything that I need
to use the LSP at install time.&lt;/p>
&lt;h1 id="nix2vim">nix2vim&lt;/h1>
&lt;p>The flake &lt;a href="https://github.com/gytis-ivaskevicius/nix2vim" target="_blank" rel="noopener">nix2vim&lt;/a> is a
framework for building a Neovim package and Lua configuration with Nix. It
comes with a function called
&lt;a href="https://github.com/gytis-ivaskevicius/nix2vim/blob/master/lib/neovim-builder.nix" target="_blank" rel="noopener">neovimBuilder&lt;/a>
that can be paired with imported modules to add plugins, &lt;code>setup&lt;/code> and &lt;code>use&lt;/code>
statements, &lt;code>vim&lt;/code> options settings all in the Nix language for translation
into Lua. You can also add &lt;code>lua&lt;/code> sections if you need to bail out to normal
Lua code.&lt;/p>
&lt;p>One of the benefits of this is that you can use Nix expressions inside of the
Neovim configuration, which means that nixpkgs can be included directly and
rendered into Nix store paths when the final output is transpiled to Lua.&lt;/p>
&lt;h1 id="lsp-configuration">LSP Configuration&lt;/h1>
&lt;p>It&amp;rsquo;s easy enough to configure the LSP and include its package at the same time. In &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/modules/common/neovim/config/lsp.nix" target="_blank" rel="noopener">my config&lt;/a>, I include the language server program alongside its settings:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>use&lt;span style="color:#f92672">.&lt;/span>lspconfig&lt;span style="color:#f92672">.&lt;/span>lua_ls&lt;span style="color:#f92672">.&lt;/span>setup &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> dsl&lt;span style="color:#f92672">.&lt;/span>callWith {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> settings &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Lua &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> diagnostics &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> globals &lt;span style="color:#f92672">=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;vim&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;hs&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> capabilities &lt;span style="color:#f92672">=&lt;/span> dsl&lt;span style="color:#f92672">.&lt;/span>rawLua &lt;span style="color:#e6db74">&amp;#34;require(&amp;#39;cmp_nvim_lsp&amp;#39;).default_capabilities()&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cmd &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>pkgs&lt;span style="color:#f92672">.&lt;/span>lua-language-server&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">/bin/lua-language-server&amp;#34;&lt;/span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In this case, I&amp;rsquo;m using &lt;code>dsl&lt;/code> functions to mimic certain Lua calls and to
bail out for more complex code. For the actual language server command, I&amp;rsquo;m
identifying the store path to the &lt;a href="https://github.com/NixOS/nixpkgs/blob/9599296566c88826a42398af0981fe065d577aa0/pkgs/development/tools/language-servers/lua-language-server/default.nix" target="_blank" rel="noopener">nixpkgs
object&lt;/a>
and including the program directly.&lt;/p>
&lt;p>When the Neovim package is built, the language server becomes a dependency, so
therefore you have a guarantee that the LSP is ready when you launch Neovim.
You can also imagine calling the Neovim package &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/modules/common/neovim/package/default.nix" target="_blank" rel="noopener">as a
function&lt;/a>
and passing the specific version of the language server based on the needs of
the project, and/or including only the language servers you need for those
projects with a project-specific Neovim.&lt;/p></description></item><item><title>Using Hammerspoon as a macOS Window Manager</title><link>https://nmasur.github.io/posts/hammerspoon-window-manager/</link><pubDate>Fri, 15 Nov 2024 10:52:53 -0700</pubDate><guid>https://nmasur.github.io/posts/hammerspoon-window-manager/</guid><description>&lt;p>Before macOS got built-in &lt;a href="https://support.apple.com/en-euro/guide/mac-help/mchlef287e5d/15.0/mac/15.0" target="_blank" rel="noopener">window tiling
features&lt;/a>
there weren&amp;rsquo;t too many great ways to control window panes.&lt;/p>
&lt;p>You either had to pay for an app like &lt;a href="https://rectangleapp.com/" target="_blank" rel="noopener">Rectangle&lt;/a> or
&lt;a href="https://magnet.crowdcafe.com/" target="_blank" rel="noopener">Magnet&lt;/a> and remember to download it whenever
you setup a new computer, or use something like
&lt;a href="https://freemacsoft.net/tiles/" target="_blank" rel="noopener">Tiles&lt;/a>, or a full-featured window manager like
&lt;a href="https://github.com/koekeishiya/yabai" target="_blank" rel="noopener">Yabai&lt;/a>.&lt;/p>
&lt;p>However, since I was already using &lt;a href="https://www.hammerspoon.org/" target="_blank" rel="noopener">Hammerspoon&lt;/a>
for other automation, I figured it would be easiest to just repurpose it as a
window management tool, partially inspired by &lt;a href="https://youtu.be/JZDt-PRq0uo?si=bo7xwzKH68G72J_Q&amp;amp;t=2507" target="_blank" rel="noopener">this
video&lt;/a>.&lt;/p>
&lt;h1 id="hammerspoon">Hammerspoon&lt;/h1>
&lt;p>&lt;a href="https://www.hammerspoon.org/" target="_blank" rel="noopener">Hammerspoon&lt;/a> is an engine that allows you to
write basic Lua scripts to interact with the UI and other elements of macOS. I
use it to develop my own special keyboard shortcuts, such as:&lt;/p>
&lt;ul>
&lt;li>A set of &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/modules/darwin/hammerspoon/Spoons/Launcher.spoon/init.lua" target="_blank" rel="noopener">quick-launch
keys&lt;/a>
for various applications.&lt;/li>
&lt;li>Convert my &lt;code>CAPSLOCK&lt;/code> key into a double &lt;code>CTRL&lt;/code>/&lt;code>ESC&lt;/code> &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/modules/darwin/hammerspoon/Spoons/ControlEscape.spoon/init.lua" target="_blank" rel="noopener">utility
key&lt;/a>.&lt;/li>
&lt;li>Dismiss alert notifications &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/modules/darwin/hammerspoon/Spoons/DismissAlerts.spoon/init.lua" target="_blank" rel="noopener">with a
keypress&lt;/a>
after I&amp;rsquo;ve read them.&lt;/li>
&lt;/ul>
&lt;h1 id="window-management">Window Management&lt;/h1>
&lt;p>Changing the window settings is pretty simple. After binding the modifiers and
key with &lt;code>hs.hotkey.bind({ &amp;quot;alt&amp;quot;, &amp;quot;ctrl&amp;quot;, &amp;quot;cmd&amp;quot; }, &amp;quot;m&amp;quot;, someFunction)&lt;/code>, you
can then get the current focused window with &lt;code>hs.window.focusedWindow()&lt;/code> and
frame with &lt;code>win:frame()&lt;/code>. If you want to maximize the window, use
&lt;code>win:screen():fullFrame()&lt;/code> to get the max size and set the frame to that
size. Divide the max width by 2 and move the origin to the left or center to
half-maximize (tile to one side).&lt;/p>
&lt;p>In my setup, I have the following keybinds:&lt;/p>
&lt;ul>
&lt;li>&lt;code>CMD + ALT + CTRL + n&lt;/code>: &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/modules/darwin/hammerspoon/Spoons/MoveWindow.spoon/init.lua#L15" target="_blank" rel="noopener">Move window to next screen&lt;/a>.&lt;/li>
&lt;li>&lt;code>CMD + ALT + CTRL + b&lt;/code>: &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/modules/darwin/hammerspoon/Spoons/MoveWindow.spoon/init.lua#L26" target="_blank" rel="noopener">Move window to previous screen&lt;/a>.&lt;/li>
&lt;li>&lt;code>CMD + ALT + CTRL + m&lt;/code>: &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/modules/darwin/hammerspoon/Spoons/MoveWindow.spoon/init.lua#L33" target="_blank" rel="noopener">Maximize window on current screen&lt;/a>.&lt;/li>
&lt;li>&lt;code>CMD + ALT + CTRL + o&lt;/code>: &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/modules/darwin/hammerspoon/Spoons/MoveWindow.spoon/init.lua#L53" target="_blank" rel="noopener">Half-maximize to the right&lt;/a>.&lt;/li>
&lt;li>&lt;code>CMD + ALT + CTRL + u&lt;/code>: &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/modules/darwin/hammerspoon/Spoons/MoveWindow.spoon/init.lua#L67" target="_blank" rel="noopener">Half-maximize to the left&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>Prebuilding and Running AWS Instances with NixOS, GitHub Actions, and Terraform</title><link>https://nmasur.github.io/posts/prebuilding-aws-nixos/</link><pubDate>Thu, 14 Nov 2024 15:58:23 -0700</pubDate><guid>https://nmasur.github.io/posts/prebuilding-aws-nixos/</guid><description>&lt;p>The typical strategy for automatically launching Linux VMs in AWS looks
something like this:&lt;/p>
&lt;ol>
&lt;li>Use &lt;a href="https://www.packer.io/" target="_blank" rel="noopener">Packer&lt;/a> to create an EC2 instance, connect to
it via SSH, run commands to configure the machine, snapshot the EC2 as an
AMI, and then terminate the instance.&lt;/li>
&lt;li>Use &lt;a href="https://www.terraform.io/" target="_blank" rel="noopener">Terraform&lt;/a> or another deployment script to
launch a new EC2 instance using the AMI as a base.&lt;/li>
&lt;li>Configure &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html" target="_blank" rel="noopener">user data
(cloud-init)&lt;/a>
to run initialization scripts on first boot.&lt;/li>
&lt;/ol>
&lt;p>This process is brittle, requires multiple steps, including launching a new EC2
instance multiple times. Instead, I like to prebuild a NixOS AMI and let
Terraform import the image into AWS and launch the machine in a single action.&lt;/p>
&lt;h1 id="prebuilding-the-image">Prebuilding the Image&lt;/h1>
&lt;p>In my flake, I use
&lt;a href="https://github.com/nix-community/nixos-generators" target="_blank" rel="noopener">nixos-generators&lt;/a> to create
an &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/flake.nix#L347" target="_blank" rel="noopener">x86-64 Amazon
image&lt;/a>
from my &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/hosts/arrow/modules.nix" target="_blank" rel="noopener">pre-configured NixOS
configuration&lt;/a>.
I use &lt;code>nix build&lt;/code> to create a &lt;code>.vhd&lt;/code> disk image file which can be uploaded to
S3 afterwards or picked up by Terraform and pushed to S3.&lt;/p>
&lt;h1 id="importing-the-ami">Importing the AMI&lt;/h1>
&lt;p>In order to generate a valid AMI, the &lt;code>.vhd&lt;/code> file must be imported directly
from S3. With Terraform, this can be done using the
&lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/hosts/arrow/aws/image.tf#L66-L80" target="_blank" rel="noopener">aws_ebs_snapshot_import&lt;/a>
resource.&lt;/p>
&lt;p>Once the AMI is imported, you can reference it in a basic
&lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/hosts/arrow/aws/ec2.tf#L1-L14" target="_blank" rel="noopener">aws_instance&lt;/a>
resource just like any other EC2 launch.&lt;/p>
&lt;h1 id="putting-it-all-together">Putting It All Together&lt;/h1>
&lt;p>Using a &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/.github/workflows/arrow-aws.yml" target="_blank" rel="noopener">GitHub Actions
workflow&lt;/a>
you can perform the following steps to automatically launch everything in one
swoop:&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/.github/workflows/arrow-aws.yml#L51-L57" target="_blank" rel="noopener">Enable
KVM&lt;/a>
in GitHub Actions in order to generate the image locally.&lt;/li>
&lt;li>&lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/.github/workflows/arrow-aws.yml#L71-L74" target="_blank" rel="noopener">Build the
image&lt;/a>
with &lt;code>nix build&lt;/code>, using a cache if available.&lt;/li>
&lt;li>&lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/.github/workflows/arrow-aws.yml#L76-L81" target="_blank" rel="noopener">Upload the image to
S3&lt;/a>,
which can also be done in Terraform instead. One reason to include this step
outside of Terraform is, if you want to sometimes skip building the image to
save time, Terraform doesn&amp;rsquo;t have to expect the image to always be there on
disk.&lt;/li>
&lt;li>&lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/.github/workflows/arrow-aws.yml#L102-L112" target="_blank" rel="noopener">Run Terraform
apply&lt;/a>
to import the image and launch the EC2 instance.&lt;/li>
&lt;li>&lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/.github/workflows/arrow-aws.yml#L126-L140" target="_blank" rel="noopener">Get the host IP and wait for SSH to be
ready&lt;/a>
in order to push content that does not belong in the Nix store (i.e. secrets).&lt;/li>
&lt;li>&lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/.github/workflows/arrow-aws.yml#L142-L154" target="_blank" rel="noopener">Push
secrets&lt;/a>
to the machine with SSH using the pre-generated &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/hosts/arrow/modules.nix#L19" target="_blank" rel="noopener">SSH key placed in the
config&lt;/a>.&lt;/li>
&lt;li>The systemd services will automatically start up, including registering
their &lt;a href="https://github.com/nmasur/dotfiles/blob/1022a3998f06819d6b7987d312d62bb7c8bbea15/modules/nixos/services/cloudflare.nix#L141-L148" target="_blank" rel="noopener">domain names with
Cloudflare&lt;/a>.&lt;/li>
&lt;/ol>
&lt;p>The instance is now running on AWS and ready to send or receive traffic!&lt;/p></description></item><item><title>Nextcloud NixOS Setup Overview</title><link>https://nmasur.github.io/posts/nextcloud-nixos-setup-overview/</link><pubDate>Tue, 06 Feb 2024 20:30:53 -0500</pubDate><guid>https://nmasur.github.io/posts/nextcloud-nixos-setup-overview/</guid><description>&lt;p>&lt;a href="https://github.com/nmasur/dotfiles/blob/e7cdfc1453649a757aab5f922ef95f69b3aea492/modules/nixos/services/nextcloud.nix" target="_blank" rel="noopener">Here is my setup&lt;/a>.&lt;/p>
&lt;p>I just use &lt;code>services.nextcloud.enable = true&lt;/code> to run Nextcloud, since I don&amp;rsquo;t run any of my services in containers or Flatpaks. Systemd feels most native to NixOS.&lt;/p>
&lt;p>You get Nextcloud, Redis, and the MySQL database basically for free. I also use the Nix-packaged Nextcloud plugins and, for any missing, I &lt;a href="https://github.com/nmasur/dotfiles/blob/e7cdfc1453649a757aab5f922ef95f69b3aea492/flake.nix#L190-L214" target="_blank" rel="noopener">pin their releases to my flake&lt;/a> and &lt;a href="https://github.com/nmasur/dotfiles/blob/e7cdfc1453649a757aab5f922ef95f69b3aea492/overlays/nextcloud-apps.nix" target="_blank" rel="noopener">add them as pkgs with an overlay&lt;/a>.&lt;/p>
&lt;p>The biggest change I made was to &lt;a href="https://github.com/nmasur/dotfiles/blob/e7cdfc1453649a757aab5f922ef95f69b3aea492/modules/nixos/services/nextcloud.nix#L38-L39" target="_blank" rel="noopener">disable nginx&lt;/a> because I already use &lt;a href="https://github.com/nmasur/dotfiles/blob/e7cdfc1453649a757aab5f922ef95f69b3aea492/modules/nixos/services/caddy.nix" target="_blank" rel="noopener">Caddy&lt;/a> as my reverse proxy for all my services. This means I had to &lt;a href="https://nmasur.github.io/posts/nextcloud-caddy-nixos/">write matching rules&lt;/a> to convert the recommended nginx config into Caddy routes.&lt;/p>
&lt;p>I wouldn&amp;rsquo;t suggest that everyone use this exact approach, but I do recommend starting with the native NixOS services and tweaking their settings to fit your needs rather than immediately jumping to containers or Flatpaks.&lt;/p></description></item><item><title>Caddy Cloudflare DNS on NixOS</title><link>https://nmasur.github.io/posts/caddy-cloudflare-dns/</link><pubDate>Sun, 10 Sep 2023 13:12:26 +0000</pubDate><guid>https://nmasur.github.io/posts/caddy-cloudflare-dns/</guid><description>&lt;p>If you serve Caddy from behind Cloudflare and enforce, you may run into an
issue with auto-provisioning ACME SSL certs.&lt;/p>
&lt;p>By default, Caddy uses the HTTP validation method which requires no setup but
does involve serving traffic on HTTP / port 80, including through Cloudflare. I
prefer Cloudflare to enforce using HTTPS / port 443 everywhere, which means
that every time I want to validate a new cert I have to temporarily disable the
HTTPS enforcement and/or set the SSL settings to &amp;ldquo;flexible&amp;rdquo; instead of
&amp;ldquo;strict&amp;rdquo;.&lt;/p>
&lt;p>For hands-free automation, this is a non-starter!&lt;/p>
&lt;p>The solution is to switch from HTTP validation to TXT validation, which doesn&amp;rsquo;t
require serving HTTP but &lt;em>does&lt;/em> require updating your DNS records to receive an
SSL cert.&lt;/p>
&lt;h2 id="updating-dns-with-caddy">Updating DNS with Caddy&lt;/h2>
&lt;p>Caddy has a &lt;a href="https://github.com/caddy-dns/cloudflare" target="_blank" rel="noopener">plugin&lt;/a> to connect to
Cloudflare&amp;rsquo;s DNS service and automatically manage the records for cert
validation, but it&amp;rsquo;s not packaged in NixOS.&lt;/p>
&lt;p>So the next step is to recompile Caddy with the Cloudflare DNS plugin using an
overlay. Here&amp;rsquo;s what my overlay looks like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>_final: prev:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> plugins &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;github.com/caddy-dns/cloudflare&amp;#34;&lt;/span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> goImports &lt;span style="color:#f92672">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> prev&lt;span style="color:#f92672">.&lt;/span>lib&lt;span style="color:#f92672">.&lt;/span>flip prev&lt;span style="color:#f92672">.&lt;/span>lib&lt;span style="color:#f92672">.&lt;/span>concatMapStrings plugins (pkg: &lt;span style="color:#e6db74">&amp;#34; _ &lt;/span>&lt;span style="color:#ae81ff">\&amp;#34;&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>pkg&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#ae81ff">\&amp;#34;\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> goGets &lt;span style="color:#f92672">=&lt;/span> prev&lt;span style="color:#f92672">.&lt;/span>lib&lt;span style="color:#f92672">.&lt;/span>flip prev&lt;span style="color:#f92672">.&lt;/span>lib&lt;span style="color:#f92672">.&lt;/span>concatMapStrings plugins
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (pkg: &lt;span style="color:#e6db74">&amp;#34;go get &lt;/span>&lt;span style="color:#e6db74">${&lt;/span>pkg&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74"> &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> main &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> package main
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> import (
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> caddycmd &amp;#34;github.com/caddyserver/caddy/v2/cmd&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> _ &amp;#34;github.com/caddyserver/caddy/v2/modules/standard&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &lt;/span>&lt;span style="color:#e6db74">${&lt;/span>goImports&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> func main() {
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> caddycmd.Main()
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#39;&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">in&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> caddy-cloudflare &lt;span style="color:#f92672">=&lt;/span> prev&lt;span style="color:#f92672">.&lt;/span>buildGo120Module {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pname &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;caddy-cloudflare&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> version &lt;span style="color:#f92672">=&lt;/span> prev&lt;span style="color:#f92672">.&lt;/span>caddy&lt;span style="color:#f92672">.&lt;/span>version;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> runVend &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subPackages &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;cmd/caddy&amp;#34;&lt;/span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> src &lt;span style="color:#f92672">=&lt;/span> prev&lt;span style="color:#f92672">.&lt;/span>caddy&lt;span style="color:#f92672">.&lt;/span>src;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vendorSha256 &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;sha256:mwIsWJYKuEZpOU38qZOG1LEh4QpK4EO0/8l4UGsroU8=&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> overrideModAttrs &lt;span style="color:#f92672">=&lt;/span> (_: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> preBuild &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> echo &amp;#39;&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>main&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#39; &amp;gt; cmd/caddy/main.go
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &lt;/span>&lt;span style="color:#e6db74">${&lt;/span>goGets&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#39;&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> postInstall &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;cp go.sum go.mod $out/ &amp;amp;&amp;amp; ls $out/&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> postPatch &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> echo &amp;#39;&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>main&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#39; &amp;gt; cmd/caddy/main.go
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> cat cmd/caddy/main.go
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#39;&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> postConfigure &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> cp vendor/go.sum ./
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> cp vendor/go.mod ./
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#39;&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> meta &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">with&lt;/span> prev&lt;span style="color:#f92672">.&lt;/span>lib; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> homepage &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;https://caddyserver.com&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> description &lt;span style="color:#f92672">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;Fast, cross-platform HTTP/2 web server with automatic HTTPS&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> license &lt;span style="color:#f92672">=&lt;/span> licenses&lt;span style="color:#f92672">.&lt;/span>asl20;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> maintainers &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">with&lt;/span> maintainers; [ Br1ght0ne techknowlogick ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The primary change we&amp;rsquo;re making here is pulling in the Go module for the
plugin, and updating the resulting &lt;code>vendorSha256&lt;/code>.&lt;/p>
&lt;p>Once we have made our overlay (I gave it a separate &lt;code>pname&lt;/code> above called
&lt;code>caddy-cloudflare&lt;/code>) we can use it in our Caddy settings.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>services&lt;span style="color:#f92672">.&lt;/span>caddy&lt;span style="color:#f92672">.&lt;/span>package &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> pkgs&lt;span style="color:#f92672">.&lt;/span>caddy-cloudflare;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You could also just override the &lt;code>caddy&lt;/code> package and not bother to update
&lt;code>services.caddy.package&lt;/code>, but I like that 1) I have to choose to use it
explicitly, and 2) I can conditionally enable this version of the package only
for machines that use Cloudflare, in case I&lt;/p>
&lt;p>In my &lt;a href="https://nmasur.github.io/posts/nextcloud-caddy-nixos/">previous post&lt;/a>, I explained that I use the
JSON config format instead of a traditional Caddyfile, which gives me more
control over options as well as the ability to merge together routes from
different services.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>services&lt;span style="color:#f92672">.&lt;/span>caddy &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> adapter &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#39;&amp;#39;&amp;#34;&lt;/span>; &lt;span style="color:#75715e"># Required to enable JSON&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> configFile &lt;span style="color:#f92672">=&lt;/span> pkgs&lt;span style="color:#f92672">.&lt;/span>writeText &lt;span style="color:#e6db74">&amp;#34;Caddyfile&amp;#34;&lt;/span> (builtins&lt;span style="color:#f92672">.&lt;/span>toJSON {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apps&lt;span style="color:#f92672">.&lt;/span>http&lt;span style="color:#f92672">.&lt;/span>servers&lt;span style="color:#f92672">.&lt;/span>main &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> listen &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;:443&amp;#34;&lt;/span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> routes &lt;span style="color:#f92672">=&lt;/span> config&lt;span style="color:#f92672">.&lt;/span>caddy&lt;span style="color:#f92672">.&lt;/span>routes;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Adding the following into the JSON configuration will enable the new Cloudflare
DNS plugin as an ACME DNS provider, which will automatically create the
necessary records to solve the LetsEncrypt verification challenges.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>apps&lt;span style="color:#f92672">.&lt;/span>tls&lt;span style="color:#f92672">.&lt;/span>automation&lt;span style="color:#f92672">.&lt;/span>policies &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> issuers &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;acme&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> challenges &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dns &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> provider &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;cloudflare&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> api_token &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;{env.CF_API_TOKEN}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> resolvers &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;1.1.1.1&amp;#34;&lt;/span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>However, we&amp;rsquo;ll need an API token for our Cloudflare account in order to make
those changes to our DNS as securely as possible.&lt;/p>
&lt;h2 id="setting-the-cloudflare-api-key">Setting the Cloudflare API Key&lt;/h2>
&lt;p>Check the &lt;a href="https://developers.cloudflare.com/fundamentals/api/get-started/create-token/" target="_blank" rel="noopener">Cloudflare
docs&lt;/a>
for how to generate an API token from your profile. For our purposes, the token
should have the following permissions:&lt;/p>
&lt;ul>
&lt;li>Zone permission: DNS -&amp;gt; Edit&lt;/li>
&lt;li>Zone resources: Include specific zone / all zones -&amp;gt; (Your zone)&lt;/li>
&lt;/ul>
&lt;p>You should take this API token and write it out into a secret text file with
the following format:&lt;/p>
&lt;pre tabindex="0">&lt;code>CF_API_TOKEN=yourtokenhere
&lt;/code>&lt;/pre>&lt;p>This is because we will load it as an environment file, so it must be a file
that a shell can evaluate.&lt;/p>
&lt;p>Next, you&amp;rsquo;ll need to securely deploy your new secret file to a location on your
disk. You could use one of the following methods:&lt;/p>
&lt;ul>
&lt;li>Manually copy the text file to a path on your target system.&lt;/li>
&lt;li>Deploy using &lt;a href="https://github.com/ryantm/agenix" target="_blank" rel="noopener">agenix&lt;/a> or
&lt;a href="https://github.com/Mic92/sops-nix" target="_blank" rel="noopener">sops-nix&lt;/a>.&lt;/li>
&lt;li>Build &lt;a href="https://xeiaso.net/blog/nixos-encrypted-secrets-2021-01-20" target="_blank" rel="noopener">your own&lt;/a>
secrets management tooling.&lt;/li>
&lt;/ul>
&lt;p>In any case, make sure the file permissions are set to be read by &lt;code>caddy:caddy&lt;/code>
(or whatever user is running the &lt;code>caddy&lt;/code> service) with 0400 access.&lt;/p>
&lt;p>Then you&amp;rsquo;ll need to set the following to load the secret as an environment file
for the systemd service:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>systemd&lt;span style="color:#f92672">.&lt;/span>services&lt;span style="color:#f92672">.&lt;/span>caddy&lt;span style="color:#f92672">.&lt;/span>serviceConfig&lt;span style="color:#f92672">.&lt;/span>EnvironmentFile &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> &lt;span style="color:#e6db74">/path/to/your/secret&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I&amp;rsquo;ve also found that I needed to enable &lt;code>CAP_NET_BIND_SERVICE&lt;/code> to grant the
systemd service the ability to bind to lower ports (such as 443):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>systemd&lt;span style="color:#f92672">.&lt;/span>services&lt;span style="color:#f92672">.&lt;/span>caddy&lt;span style="color:#f92672">.&lt;/span>serviceConfig&lt;span style="color:#f92672">.&lt;/span>AmbientCapabilities &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;CAP_NET_BIND_SERVICE&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>At this point, your Caddy service should be able to modify DNS records and
receive LetsEncrypt certificates automatically! Take a look at &lt;a href="https://github.com/nmasur/dotfiles/blob/d2b1d9528136032788d5cfb59f1f36f73b08d7ec/modules/nixos/services/cloudflare.nix" target="_blank" rel="noopener">my Cloudflare
configuration
settings&lt;/a>
and &lt;a href="https://github.com/nmasur/dotfiles/blob/d2b1d9528136032788d5cfb59f1f36f73b08d7ec/overlays/caddy.nix" target="_blank" rel="noopener">my Caddy
overlay&lt;/a>
as examples.&lt;/p></description></item><item><title>Nextcloud Behind Caddy on NixOS</title><link>https://nmasur.github.io/posts/nextcloud-caddy-nixos/</link><pubDate>Tue, 29 Aug 2023 11:56:26 +0000</pubDate><guid>https://nmasur.github.io/posts/nextcloud-caddy-nixos/</guid><description>&lt;p>Nextcloud can technically run behind &lt;a href="https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/reverse_proxy_configuration.html" target="_blank" rel="noopener">any reverse
proxy&lt;/a>
but their documentation is sparse and don&amp;rsquo;t include full examples.&lt;/p>
&lt;p>The Nextcloud &lt;a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/nixos/modules/services/web-apps/nextcloud.nix" target="_blank" rel="noopener">NixOS
module&lt;/a>
automatically enables nginx by default. Since I run Caddy bound to 443 for the
rest of my services, I didn&amp;rsquo;t want to have to proxy to a local nginx service
just to hit Nextcloud. They have &lt;a href="https://nixos.org/manual/nixos/stable/index.html#module-services-nextcloud-httpd" target="_blank" rel="noopener">some
documentation&lt;/a>
for using Apache httpd, but I had to adjust for Caddy.&lt;/p>
&lt;p>&lt;a href="https://github.com/nmasur/dotfiles/blob/67251a6d8d67805b6bfa15016d6960b215550a47/modules/nixos/services/nextcloud.nix" target="_blank" rel="noopener">Here&lt;/a>
is my full configuration on my system, but I&amp;rsquo;ll break it down to explain it in
detail.&lt;/p>
&lt;p>First, after enabling Nextcloud, you should at least have the following in your
configuration.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>services&lt;span style="color:#f92672">.&lt;/span>nextcloud&lt;span style="color:#f92672">.&lt;/span>config&lt;span style="color:#f92672">.&lt;/span>trustedProxies &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;127.0.0.1&amp;#34;&lt;/span> ];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This allows any local reverse proxy (including nginx, Caddy, etc.) to connect
to the Nextcloud service.&lt;/p>
&lt;p>Next, we disable nginx, since enabling the Nextcloud module will enable nginx
by default:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>services&lt;span style="color:#f92672">.&lt;/span>nginx&lt;span style="color:#f92672">.&lt;/span>enable &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> &lt;span style="color:#66d9ef">false&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We have to make sure that Caddy&amp;rsquo;s user will have access to talk to the
Nextcloud PHP service and access to the files served by Nextcloud.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>services&lt;span style="color:#f92672">.&lt;/span>phpfpm&lt;span style="color:#f92672">.&lt;/span>pools&lt;span style="color:#f92672">.&lt;/span>nextcloud&lt;span style="color:#f92672">.&lt;/span>settings &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;listen.owner&amp;#34;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> config&lt;span style="color:#f92672">.&lt;/span>services&lt;span style="color:#f92672">.&lt;/span>caddy&lt;span style="color:#f92672">.&lt;/span>user;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;listen.group&amp;#34;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> config&lt;span style="color:#f92672">.&lt;/span>services&lt;span style="color:#f92672">.&lt;/span>caddy&lt;span style="color:#f92672">.&lt;/span>group;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>users&lt;span style="color:#f92672">.&lt;/span>users&lt;span style="color:#f92672">.&lt;/span>caddy&lt;span style="color:#f92672">.&lt;/span>extraGroups &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;nextcloud&amp;#34;&lt;/span> ];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>(Your users and groups for Caddy and Nextcloud may be different, so keep that
in mind).&lt;/p>
&lt;p>The next step is where things get serious because we have to generate Caddy&amp;rsquo;s
config file. You could use the Caddyfile format for brevity, but I have chosen
to generate a JSON file for a few reasons:&lt;/p>
&lt;ol>
&lt;li>The format is easier to organize and manipulate from Nix expressions. I am
serving multiple sites from the same Caddy service, so combining all routes
into a single JSON file is cleanest for me with Nix.&lt;/li>
&lt;li>The JSON format has &lt;a href="https://caddyserver.com/docs/json/" target="_blank" rel="noopener">more options&lt;/a> that
can give you more control over settings, especially when they differ between
routes or hostnames.&lt;/li>
&lt;/ol>
&lt;p>My general Caddy setup looks like this — first, I establish a list of sets
option called &lt;code>caddy.routes&lt;/code>, so I can add them from services in multiple
places:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>options&lt;span style="color:#f92672">.&lt;/span>caddy&lt;span style="color:#f92672">.&lt;/span>routes &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> lib&lt;span style="color:#f92672">.&lt;/span>mkOption {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> type &lt;span style="color:#f92672">=&lt;/span> lib&lt;span style="color:#f92672">.&lt;/span>types&lt;span style="color:#f92672">.&lt;/span>listOf lib&lt;span style="color:#f92672">.&lt;/span>types&lt;span style="color:#f92672">.&lt;/span>attrs;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> description &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Caddy JSON routes for http servers&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> default &lt;span style="color:#f92672">=&lt;/span> [ ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then I use this for my base Caddy config, sticking the routes into
&lt;code>apps.http.servers.&amp;lt;something&amp;gt;.routes&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>services&lt;span style="color:#f92672">.&lt;/span>caddy &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> adapter &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#39;&amp;#39;&amp;#34;&lt;/span>; &lt;span style="color:#75715e"># Required to enable JSON&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> configFile &lt;span style="color:#f92672">=&lt;/span> pkgs&lt;span style="color:#f92672">.&lt;/span>writeText &lt;span style="color:#e6db74">&amp;#34;Caddyfile&amp;#34;&lt;/span> (builtins&lt;span style="color:#f92672">.&lt;/span>toJSON {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apps&lt;span style="color:#f92672">.&lt;/span>http&lt;span style="color:#f92672">.&lt;/span>servers&lt;span style="color:#f92672">.&lt;/span>main &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> listen &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;:443&amp;#34;&lt;/span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> routes &lt;span style="color:#f92672">=&lt;/span> config&lt;span style="color:#f92672">.&lt;/span>caddy&lt;span style="color:#f92672">.&lt;/span>routes;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The routes themselves are mostly written like the following, and can be added
anywhere in your config (even multiple times, which will merge into one list
automatically):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>caddy&lt;span style="color:#f92672">.&lt;/span>routes &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> match &lt;span style="color:#f92672">=&lt;/span> [{ host &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;my.host.name&amp;#34;&lt;/span> ]; }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handle &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;reverse_proxy&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> upstreams &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dial &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;localhost:1234&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In the above example, Caddy will match on the public hostname of your service,
and then act as a reverse-proxy by connecting to the local port &lt;code>1234&lt;/code> on your
machine where the service is running. This is how I setup most of my web
services.&lt;/p>
&lt;p>With Nextcloud, things will be a little different. The best Caddy reference I
found was &lt;a href="https://www.reddit.com/r/Nextcloud/comments/gn7fdl/looking_for_caddy_v2_sample_config_for_nextcloud/frjj50c/" target="_blank" rel="noopener">this Reddit
comment&lt;/a>
with a working Caddyfile. However, we need to translate it for our JSON
configuration.&lt;/p>
&lt;p>Step one is to set a single route with everything inside the handle attribute.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>caddy&lt;span style="color:#f92672">.&lt;/span>routes &lt;span style="color:#960050;background-color:#1e0010">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> match &lt;span style="color:#f92672">=&lt;/span> [{ host &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;your.nextcloud.domain&amp;#34;&lt;/span> ]; }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handle &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;subroute&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> routes &lt;span style="color:#f92672">=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># ... Everything goes in here&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> terminal &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>All of our Nextcloud routes I describe below will be &lt;em>subroutes&lt;/em> of the
original handle, which matches on the hostname of our Nextcloud service.&lt;/p>
&lt;p>The first subroute sets variables and headers for the other subroutes. The
variable is required for working with apps, and the header enforces the use of
HTTPS on the browser.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handle &lt;span style="color:#f92672">=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;vars&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> root &lt;span style="color:#f92672">=&lt;/span> config&lt;span style="color:#f92672">.&lt;/span>services&lt;span style="color:#f92672">.&lt;/span>nextcloud&lt;span style="color:#f92672">.&lt;/span>package;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;headers&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response&lt;span style="color:#f92672">.&lt;/span>set&lt;span style="color:#f92672">.&lt;/span>Strict-Transport-Security &lt;span style="color:#f92672">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ &lt;span style="color:#e6db74">&amp;#34;max-age=31536000;&amp;#34;&lt;/span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The next subroute makes use of this &lt;code>root&lt;/code> variable to serve content from the
&lt;code>/nix-apps/&lt;/code> and &lt;code>/store-apps/&lt;/code> directories, which are the built-in and
marketplace plugins for Nextcloud. You will likely need this even if you are
just using Nextcloud out of the box.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> match &lt;span style="color:#f92672">=&lt;/span> [{ path &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;/nix-apps*&amp;#34;&lt;/span> &lt;span style="color:#e6db74">&amp;#34;/store-apps*&amp;#34;&lt;/span> ]; }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handle &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;vars&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> root &lt;span style="color:#f92672">=&lt;/span> config&lt;span style="color:#f92672">.&lt;/span>services&lt;span style="color:#f92672">.&lt;/span>nextcloud&lt;span style="color:#f92672">.&lt;/span>home;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next, we have a subroute for managing CardDAV and CalDAV traffic (for contacts,
calendars). These URLs are redirected to a specific PHP location explained in
the &lt;a href="https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/reverse_proxy_configuration.html#service-discovery" target="_blank" rel="noopener">official
docs&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> match &lt;span style="color:#f92672">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [{ path &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;/.well-known/carddav&amp;#34;&lt;/span> &lt;span style="color:#e6db74">&amp;#34;/.well-known/caldav&amp;#34;&lt;/span> ]; }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handle &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;static_response&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers &lt;span style="color:#f92672">=&lt;/span> { Location &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;/remote.php/dav&amp;#34;&lt;/span> ]; };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> status_code &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">301&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The next subroute is used to deny access to sensitive or internal-only files by
returning a 404 error response.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> match &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> path &lt;span style="color:#f92672">=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/.htaccess&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/data/*&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/config/*&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/db_structure&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/.xml&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/README&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/3rdparty/*&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/lib/*&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/templates/*&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/occ&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/console.php&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handle &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;static_response&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> status_code &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">404&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We also need a subroute to redirect &lt;code>/index.php&lt;/code> requests to the base homepage
instead of trying to use a PHP file that doesn&amp;rsquo;t exist.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> match &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> file &lt;span style="color:#f92672">=&lt;/span> { try_files &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;{http.request.uri.path}/index.php&amp;#34;&lt;/span> ]; };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> not &lt;span style="color:#f92672">=&lt;/span> [{ path &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;*/&amp;#34;&lt;/span> ]; }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handle &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;static_response&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers &lt;span style="color:#f92672">=&lt;/span> { Location &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;{http.request.orig_uri.path}/&amp;#34;&lt;/span> ]; };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> status_code &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">308&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For incoming requests, we rewrite their paths to be relative to the current
directory.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> match &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> file &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> split_path &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;.php&amp;#34;&lt;/span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> try_files &lt;span style="color:#f92672">=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;{http.request.uri.path}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;{http.request.uri.path}/index.php&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;index.php&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handle &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;rewrite&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uri &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;{http.matchers.file.relative}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Of course, we eventually have to send PHP traffic to the actual PHP service,
which in this case is not listening on a local port by default, but on a UNIX
socket.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> match &lt;span style="color:#f92672">=&lt;/span> [{ path &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;*.php&amp;#34;&lt;/span> ]; }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handle &lt;span style="color:#f92672">=&lt;/span> [{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;reverse_proxy&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> transport &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protocol &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;fastcgi&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> split_path &lt;span style="color:#f92672">=&lt;/span> [ &lt;span style="color:#e6db74">&amp;#34;.php&amp;#34;&lt;/span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> upstreams &lt;span style="color:#f92672">=&lt;/span> [{ dial &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;unix//run/phpfpm/nextcloud.sock&amp;#34;&lt;/span>; }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Finally, all other requests are served simply as static files:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nix" data-lang="nix">&lt;span style="display:flex;">&lt;span>{ handle &lt;span style="color:#f92672">=&lt;/span> [{ handler &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;file_server&amp;#34;&lt;/span>; }]; }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it! Now Caddy will serve as the ingress to Nextcloud directly.&lt;/p></description></item></channel></rss>