diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,25 @@
 # Changelog for sandwich-contexts
 
-## Unreleased
+## 0.3.0.3
 
+* Use --no-link in Nix builds to avoid unnecessary result symlinks.
+* Make withProxyToUnixSocket more robust with a timeout.
+* Add nixpkgsRelease2411, nixpkgsRelease2505, nixpkgsMaster.
+* Export findFirstFileInDirs, and make tryFindBinary search under Applications on macOS.
+* Add buildNixPackage functions + remove a bunch of Control.Monad.Catch usage.
+
+## 0.3.0.2
+
 * Add `postgresNixConfExtraLines` to `PostgresNixOptions`, to be able to add extra lines to `postgresql.conf`.
+* Add 'fakeSmtpServerHostname' to FakeSmtpServer.
+* Add some generic options for launching containers.
+* Enable Unix socket based contexts on Windows.
+* Add withPostgresViaNix', withPostgresUnixSocketViaNix'.
+* Add makeNixContext, makeNixContext'.
 
 ## 0.3.0.1
 
-* Automatically find sufficiently short Unix socket paths when the system temp ones are too long
+* Automatically find sufficiently short Unix socket paths when the system temp ones are too long.
 
 ## 0.3.0.0
 
diff --git a/lib/Test/Sandwich/Contexts/FakeSmtpServer.hs b/lib/Test/Sandwich/Contexts/FakeSmtpServer.hs
--- a/lib/Test/Sandwich/Contexts/FakeSmtpServer.hs
+++ b/lib/Test/Sandwich/Contexts/FakeSmtpServer.hs
@@ -34,7 +34,6 @@
   ) where
 
 import Control.Monad
-import Control.Monad.Catch (MonadMask, MonadThrow)
 import Control.Monad.IO.Unlift
 import Control.Monad.Logger
 import Control.Monad.Reader
@@ -95,7 +94,7 @@
   -- | The port on which the fake SMTP server is running.
   , fakeSmtpServerSmtpPort :: PortNumber
   -- | Callback to retrieve the emails the server has received.
-  , fakeSmtpServerGetEmails :: forall m. (MonadLoggerIO m, MonadUnliftIO m, MonadThrow m) => m [EmailInfo]
+  , fakeSmtpServerGetEmails :: forall m. (MonadLoggerIO m, MonadUnliftIO m) => m [EmailInfo]
   }
 
 fakeSmtpServer :: Label "fakeSmtpServer" FakeSmtpServer
@@ -103,7 +102,7 @@
 
 -- * Functions
 
-type BaseMonad context m = (HasBaseContext context, MonadMask m, MonadUnliftIO m)
+type BaseMonad context m = (HasBaseContext context, MonadUnliftIO m)
 
 type FakeSmtpServerContext context =
   LabelValue "fakeSmtpServer" FakeSmtpServer
@@ -215,7 +214,7 @@
       Just n -> pure n
 
 getEmails :: (
-  MonadLoggerIO m, MonadUnliftIO m, MonadThrow m
+  MonadLoggerIO m, MonadUnliftIO m
   ) => Manager -> Text -> PortNumber -> m [EmailInfo]
 getEmails manager authPart httpPort = do
   req <- liftIO $ parseRequest [i|http://#{authPart}localhost:#{httpPort}/api/emails|]
diff --git a/lib/Test/Sandwich/Contexts/Files.hs b/lib/Test/Sandwich/Contexts/Files.hs
--- a/lib/Test/Sandwich/Contexts/Files.hs
+++ b/lib/Test/Sandwich/Contexts/Files.hs
@@ -66,6 +66,7 @@
   -- * Helpers for file-finding callbacks
   , defaultFindFile
   , findFirstFile
+  , findFirstFileInDirs
 
   -- * Low-level
   , mkFileLabel
@@ -77,7 +78,6 @@
   , FileValue
   ) where
 
-import Control.Monad.Catch (MonadMask)
 import Control.Monad.IO.Unlift
 import Control.Monad.Logger
 import Control.Monad.Trans.Except
@@ -85,6 +85,7 @@
 import GHC.TypeLits
 import Relude
 import System.FilePath
+import System.Info (os)
 import Test.Sandwich
 import Test.Sandwich.Contexts.Files.Types
 import Test.Sandwich.Contexts.Nix
@@ -154,136 +155,148 @@
 -- | Introduce a given 'EnvironmentFile' from the 'NixContext' in scope.
 -- It's recommended to use this with -XTypeApplications.
 introduceFileViaNixPackage :: forall a context m. (
-  HasBaseContext context, HasNixContext context, MonadUnliftIO m, KnownSymbol a
-  ) =>
-    -- | Nix package name which contains the desired file.
-    -- This package will be evaluated using the configured Nixpkgs version of the 'NixContext'.
-    NixPackageName
-    -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
-    -> SpecFree context m ()
+  HasBaseContext context, HasNixContext context
+  , MonadUnliftIO m, KnownSymbol a
+  )
+  -- | Nix package name which contains the desired file.
+  -- This package will be evaluated using the configured Nixpkgs version of the 'NixContext'.
+  => NixPackageName
+  -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
+  -> SpecFree context m ()
 introduceFileViaNixPackage name = introduceFileViaNixPackage' @a name (defaultFindFile (symbolVal (Proxy @a)))
 
 -- | Same as 'introduceFileViaNixPackage', but allows you to customize the search callback.
 introduceFileViaNixPackage' :: forall a context m. (
-  HasBaseContext context, HasNixContext context, MonadUnliftIO m, KnownSymbol a
-  ) =>
-    -- | Nix package name which contains the desired file.
-    -- This package will be evaluated using the configured Nixpkgs version of the 'NixContext'.
-    NixPackageName
-    -- | Callback to find the desired file within the Nix derivation path.
-    -- It will be passed the derivation path, and should return the file. For example,
-    -- tryFindFile "\/nix\/store\/...selenium-server-standalone-3.141.59" may return
-    -- "\/nix\/store\/...selenium-server-standalone-3.141.59\/share\/lib\/selenium-server-standalone-3.141.59\/selenium-server-standalone-3.141.59.jar".
-    -> (FilePath -> IO FilePath)
-    -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
-    -> SpecFree context m ()
+  HasBaseContext context, HasNixContext context
+  , MonadUnliftIO m, KnownSymbol a
+  )
+  -- | Nix package name which contains the desired file.
+  -- This package will be evaluated using the configured Nixpkgs version of the 'NixContext'.
+  => NixPackageName
+  -- | Callback to find the desired file within the Nix derivation path.
+  -- It will be passed the derivation path, and should return the file. For example,
+  -- tryFindFile "\/nix\/store\/...selenium-server-standalone-3.141.59" may return
+  -- "\/nix\/store\/...selenium-server-standalone-3.141.59\/share\/lib\/selenium-server-standalone-3.141.59\/selenium-server-standalone-3.141.59.jar".
+  -> (FilePath -> IO FilePath)
+  -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
+  -> SpecFree context m ()
 introduceFileViaNixPackage' = introduceFileViaNixPackage'' (defaultNodeOptions { nodeOptionsVisibilityThreshold = defaultFileContextVisibilityThreshold })
 
 -- | Same as 'introduceFileViaNixPackage'', but allows passing custom 'NodeOptions'.
 introduceFileViaNixPackage'' :: forall a context m. (
-  HasBaseContext context, HasNixContext context, MonadUnliftIO m, KnownSymbol a
-  ) => NodeOptions
-    -- | Nix package name which contains the desired file.
-    -> NixPackageName
-    -- | Callback to find the desired file within the Nix derivation path.
-    -- It will be passed the derivation path, and should return the file. For example,
-    -- tryFindFile "\/nix\/store\/...selenium-server-standalone-3.141.59" may return
-    -- "\/nix\/store\/...selenium-server-standalone-3.141.59\/share\/lib\/selenium-server-standalone-3.141.59\/selenium-server-standalone-3.141.59.jar".
-    -> (FilePath -> IO FilePath)
-    -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
-    -> SpecFree context m ()
+  HasBaseContext context, HasNixContext context
+  , MonadUnliftIO m, KnownSymbol a
+  )
+  -- | Nix package name which contains the desired file.
+  => NodeOptions
+  -> NixPackageName
+  -- | Callback to find the desired file within the Nix derivation path.
+  -- It will be passed the derivation path, and should return the file. For example,
+  -- tryFindFile "\/nix\/store\/...selenium-server-standalone-3.141.59" may return
+  -- "\/nix\/store\/...selenium-server-standalone-3.141.59\/share\/lib\/selenium-server-standalone-3.141.59\/selenium-server-standalone-3.141.59.jar".
+  -> (FilePath -> IO FilePath)
+  -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
+  -> SpecFree context m ()
 introduceFileViaNixPackage'' nodeOptions packageName tryFindFile = introduce' nodeOptions [i|#{binaryName} (file via Nix package #{packageName})|] (mkFileLabel @a) alloc (const $ return ())
   where
     binaryName :: String
     binaryName = symbolVal (Proxy @a)
 
-    alloc = buildNixSymlinkJoin [packageName] >>= \p -> EnvironmentFile <$> liftIO (tryFindFile p)
+    alloc = buildNixPackage packageName >>= \p -> EnvironmentFile <$> liftIO (tryFindFile p)
 
 -- | Lower-level version of 'introduceFileViaNixPackage'.
 getFileViaNixPackage :: forall context m. (
   HasBaseContextMonad context m, HasNixContext context
   , MonadUnliftIO m, MonadLoggerIO m
-  ) =>
-    -- | Nix package name which contains the desired file.
-    NixPackageName
-    -- | Callback to find the desired file, as in 'introduceFileViaNixPackage'.
-    -> (FilePath -> IO FilePath)
-    -> m FilePath
-getFileViaNixPackage packageName tryFindFile = buildNixSymlinkJoin [packageName] >>= liftIO . tryFindFile
+  )
+  -- | Nix package name which contains the desired file.
+  => NixPackageName
+  -- | Callback to find the desired file, as in 'introduceFileViaNixPackage'.
+  -> (FilePath -> IO FilePath)
+  -> m FilePath
+getFileViaNixPackage packageName tryFindFile = buildNixPackage packageName >>= liftIO . tryFindFile
 
 -- | Introduce a given 'EnvironmentFile' from the 'NixContext' in scope.
 -- It's recommended to use this with -XTypeApplications.
+--
+-- This will search for an executable file with the given name in the "bin" directory of the Nix output.
+-- On MacOS, it will also recursively search the "Applications" directory. This makes it seamless when you
+-- want to work with a derivation like "firefox", which is bundled as an application on macOS.
 introduceBinaryViaNixPackage :: forall a context m. (
-  HasBaseContext context, HasNixContext context, MonadUnliftIO m, KnownSymbol a
-  ) =>
-    -- | Nix package name which contains the desired binary.
-    -- This package will be evaluated using the configured Nixpkgs version of the 'NixContext'.
-    -- For example, you can use the "hello" binary from the "hello" package like this:
-    --
-    -- introduceBinaryViaNixPackage' @"hello" "hello"
-    NixPackageName
-    -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
-    -> SpecFree context m ()
+  HasBaseContext context, HasNixContext context
+  , MonadUnliftIO m, KnownSymbol a
+  )
+  -- | Nix package name which contains the desired binary.
+  -- This package will be evaluated using the configured Nixpkgs version of the 'NixContext'.
+  -- For example, you can use the "hello" binary from the "hello" package like this:
+  --
+  -- introduceBinaryViaNixPackage' @"hello" "hello"
+  => NixPackageName
+  -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
+  -> SpecFree context m ()
 introduceBinaryViaNixPackage = introduceBinaryViaNixPackage' @a (defaultNodeOptions { nodeOptionsVisibilityThreshold = defaultFileContextVisibilityThreshold })
 
 -- | Same as 'introduceBinaryViaNixPackage', but allows passing custom 'NodeOptions'.
 introduceBinaryViaNixPackage' :: forall a context m. (
-  HasBaseContext context, HasNixContext context, MonadUnliftIO m, KnownSymbol a
-  ) => NodeOptions
-    -- | Nix package name which contains the desired binary.
-    -> NixPackageName
-    -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
-    -> SpecFree context m ()
+  HasBaseContext context, HasNixContext context
+  , MonadUnliftIO m, KnownSymbol a
+  )
+  -- | Nix package name which contains the desired binary.
+  => NodeOptions
+  -> NixPackageName
+  -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
+  -> SpecFree context m ()
 introduceBinaryViaNixPackage' nodeOptions packageName = introduce' nodeOptions [i|#{binaryName} (binary via Nix package #{packageName})|] (mkFileLabel @a) alloc (const $ return ())
   where
     binaryName :: String
     binaryName = symbolVal (Proxy @a)
 
-    alloc = buildNixSymlinkJoin [packageName] >>= tryFindBinary binaryName
+    alloc = buildNixPackage packageName >>= tryFindBinary binaryName
 
 -- | Lower-level version of 'introduceBinaryViaNixPackage'.
 getBinaryViaNixPackage :: forall a context m. (
   HasBaseContextMonad context m, HasNixContext context
   , MonadUnliftIO m, MonadLoggerIO m, KnownSymbol a
-  ) =>
-    -- | Nix package name which contains the desired binary.
-    NixPackageName
-    -> m FilePath
+  )
+  -- | Nix package name which contains the desired binary.
+  => NixPackageName
+  -> m FilePath
 getBinaryViaNixPackage packageName = do
-  unEnvironmentFile <$> (buildNixSymlinkJoin [packageName] >>= tryFindBinary (symbolVal (Proxy @a)))
+  unEnvironmentFile <$> (buildNixPackage packageName >>= tryFindBinary (symbolVal (Proxy @a)))
 
 -- | Lower-level version of 'introduceBinaryViaNixPackage'.
 getBinaryViaNixPackage' :: forall a context m. (
   HasBaseContext context, MonadReader context m
   , MonadLogger m, MonadUnliftIO m, KnownSymbol a
-  ) =>
-    -- | 'NixContext' to use.
-    NixContext
-    -- | Nix package name which contains the desired binary.
-    -> NixPackageName
-    -> m FilePath
+  )
+  -- | 'NixContext' to use.
+  => NixContext
+  -- | Nix package name which contains the desired binary.
+  -> NixPackageName
+  -> m FilePath
 getBinaryViaNixPackage' nc packageName = do
-  unEnvironmentFile <$> (buildNixSymlinkJoin' nc [packageName] >>= tryFindBinary (symbolVal (Proxy @a)))
+  unEnvironmentFile <$> (buildNixPackage' nc packageName >>= tryFindBinary (symbolVal (Proxy @a)))
 
 -- | Introduce a given 'EnvironmentFile' from the 'NixContext' in scope.
 -- It's recommended to use this with -XTypeApplications.
 introduceBinaryViaNixDerivation :: forall a context m. (
-  HasBaseContext context, HasNixContext context, MonadUnliftIO m, MonadMask m, KnownSymbol a
-  ) =>
-    -- | Nix derivation as a string.
-    Text
-    -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
-    -> SpecFree context m ()
+  HasBaseContext context, HasNixContext context, MonadUnliftIO m, KnownSymbol a
+  )
+  -- | Nix derivation as a string.
+  => Text
+  -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
+  -> SpecFree context m ()
 introduceBinaryViaNixDerivation = introduceBinaryViaNixDerivation' (defaultNodeOptions { nodeOptionsVisibilityThreshold = defaultFileContextVisibilityThreshold })
 
 -- | Same as 'introduceBinaryViaNixDerivation', but allows passing custom 'NodeOptions'.
 introduceBinaryViaNixDerivation' :: forall a context m. (
-  HasBaseContext context, HasNixContext context, MonadUnliftIO m, MonadMask m, KnownSymbol a
-  ) => NodeOptions
-    -- | Nix derivation as a string.
-    -> Text
-    -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
-    -> SpecFree context m ()
+  HasBaseContext context, HasNixContext context, MonadUnliftIO m, KnownSymbol a
+  )
+  -- | Nix derivation as a string.
+  => NodeOptions
+  -> Text
+  -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
+  -> SpecFree context m ()
 introduceBinaryViaNixDerivation' nodeOptions derivation = introduce' nodeOptions [i|#{binaryName} (binary via Nix derivation)|] (mkFileLabel @a) alloc (const $ return ())
   where
     binaryName :: String
@@ -294,18 +307,18 @@
 -- | Lower-level version of 'introduceBinaryViaNixDerivation'.
 getBinaryViaNixDerivation :: forall a context m. (
   HasBaseContextMonad context m, HasNixContext context
-  , MonadUnliftIO m, MonadLoggerIO m, MonadMask m, KnownSymbol a
-  ) =>
-    -- | Nix derivation as a string.
-    Text
-    -> m FilePath
+  , MonadUnliftIO m, MonadLoggerIO m, KnownSymbol a
+  )
+  -- | Nix derivation as a string.
+  => Text
+  -> m FilePath
 getBinaryViaNixDerivation derivation =
   unEnvironmentFile <$> (buildNixCallPackageDerivation derivation >>= tryFindBinary (symbolVal (Proxy @a)))
 
 -- | Lower-level version of 'getBinaryViaNixDerivation'.
 getBinaryViaNixDerivation' :: forall a context m. (
   HasBaseContextMonad context m
-  , MonadUnliftIO m, MonadLoggerIO m, MonadMask m, KnownSymbol a
+  , MonadUnliftIO m, MonadLoggerIO m, KnownSymbol a
   )
   -- | Nix context.
   => NixContext
@@ -318,36 +331,37 @@
 -- | Introduce a given 'EnvironmentFile' from the 'NixContext' in scope.
 -- It's recommended to use this with -XTypeApplications.
 introduceFileViaNixDerivation :: forall a context m. (
-  HasBaseContext context, HasNixContext context, MonadUnliftIO m, MonadMask m, KnownSymbol a
-  ) =>
-    -- | Nix derivation as a string.
-    Text
-    -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
-    -> SpecFree context m ()
+  HasBaseContext context, HasNixContext context, MonadUnliftIO m, KnownSymbol a
+  )
+  -- | Nix derivation as a string.
+  => Text
+  -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
+  -> SpecFree context m ()
 introduceFileViaNixDerivation derivation = introduceFileViaNixDerivation' @a derivation (defaultFindFile (symbolVal (Proxy @a)))
 
 -- | Same as 'introduceFileViaNixDerivation', but allows configuring the file finding callback.
 introduceFileViaNixDerivation' :: forall a context m. (
-  HasBaseContext context, HasNixContext context, MonadUnliftIO m, MonadMask m, KnownSymbol a
-  ) =>
-    -- | Nix derivation as a string.
-    Text
-    -- | Callback to find the desired file.
-    -> (FilePath -> IO FilePath)
-    -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
-    -> SpecFree context m ()
+  HasBaseContext context, HasNixContext context, MonadUnliftIO m, KnownSymbol a
+  )
+  -- | Nix derivation as a string.
+  => Text
+  -- | Callback to find the desired file.
+  -> (FilePath -> IO FilePath)
+  -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
+  -> SpecFree context m ()
 introduceFileViaNixDerivation' = introduceFileViaNixDerivation'' (defaultNodeOptions { nodeOptionsVisibilityThreshold = defaultFileContextVisibilityThreshold })
 
 -- | Same as 'introduceFileViaNixDerivation'', but allows passing custom 'NodeOptions'.
 introduceFileViaNixDerivation'' :: forall a context m. (
-  HasBaseContext context, HasNixContext context, MonadUnliftIO m, MonadMask m, KnownSymbol a
-  ) => NodeOptions
-    -- | Nix derivation as a string.
-    -> Text
-    -- | Callback to find the desired file.
-    -> (FilePath -> IO FilePath)
-    -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
-    -> SpecFree context m ()
+  HasBaseContext context, HasNixContext context, MonadUnliftIO m, KnownSymbol a
+  )
+  -- | Nix derivation as a string.
+  => NodeOptions
+  -> Text
+  -- | Callback to find the desired file.
+  -> (FilePath -> IO FilePath)
+  -> SpecFree (LabelValue (AppendSymbol "file-" a) (EnvironmentFile a) :> context) m ()
+  -> SpecFree context m ()
 introduceFileViaNixDerivation'' nodeOptions derivation tryFindFile = introduce' nodeOptions [i|#{binaryName} (file via Nix derivation)|] (mkFileLabel @a) alloc (const $ return ())
   where
     binaryName :: String
@@ -358,21 +372,33 @@
 -- | Lower-level version of 'introduceFileViaNixDerivation'.
 getFileViaNixDerivation :: forall context m. (
   HasBaseContextMonad context m, HasNixContext context
-  , MonadUnliftIO m, MonadLoggerIO m, MonadMask m
-  ) =>
-    -- | Nix derivation as a string.
-    Text
-    -- | Callback to find the desired file.
-    -> (FilePath -> IO FilePath)
-    -> m FilePath
+  , MonadUnliftIO m, MonadLoggerIO m
+  )
+  -- | Nix derivation as a string.
+  => Text
+  -- | Callback to find the desired file.
+  -> (FilePath -> IO FilePath)
+  -> m FilePath
 getFileViaNixDerivation derivation tryFindFile = buildNixCallPackageDerivation derivation >>= liftIO . tryFindFile
 
 
+-- | Default binary search function used in 'getBinaryViaNixPackage' etc.
 tryFindBinary :: (MonadIO m) => String -> FilePath -> m (EnvironmentFile a)
-tryFindBinary binaryName env = do
-  findExecutablesInDirectories [env </> "bin"] binaryName >>= \case
-    (x:_) -> return $ EnvironmentFile x
-    _ -> expectationFailure [i|Couldn't find binary '#{binaryName}' in #{env </> "bin"}|]
+tryFindBinary binaryName env = case os of
+  "darwin" -> do
+    dirsToSearch <- filterM doesDirectoryExist [env </> "bin", env </> "Applications"]
+    let predicate x = andM [(isExecutable x), (return (takeFileName x == binaryName))]
+    -- We use own own implementation here because we want to recursively search under "Applications"
+    EnvironmentFile <$> liftIO (findFirstFileInDirs predicate dirsToSearch)
+    where
+      isExecutable :: FilePath -> IO Bool
+      isExecutable path = executable <$> getPermissions path
+  _ -> do
+    -- This is the implementation from System.Directory, which is *not* recursive and is more performant
+    -- than 'findFirstFile' because it doesn't list directories.
+    findExecutablesInDirectories [env </> "bin"] binaryName >>= \case
+      (x:_) -> return $ EnvironmentFile x
+      _ -> expectationFailure [i|Couldn't find binary '#{binaryName}' in #{env </> "bin"}|]
 
 -- | Find a file whose name exactly matches a string, using 'findFirstFile'.
 -- This calls 'takeFileName', so it only matches against the name, not the relative path.
@@ -383,9 +409,15 @@
 -- Note that the callback receives the full relative path to the file from the root dir.
 -- Throws using 'expectationFailure' when the file is not found.
 findFirstFile :: (FilePath -> IO Bool) -> FilePath -> IO FilePath
-findFirstFile predicate dir = runExceptT (go dir) >>= \case
+findFirstFile predicate dir = findFirstFileInDirs predicate [dir]
+
+-- | Find the first file under the given directory (recursively) which matches the predicate.
+-- Note that the callback receives the full relative path to the file from the root dir.
+-- Throws using 'expectationFailure' when the file is not found.
+findFirstFileInDirs :: (FilePath -> IO Bool) -> [FilePath] -> IO FilePath
+findFirstFileInDirs predicate dirs = runExceptT (sequence_ (fmap go dirs)) >>= \case
   Left x -> return x
-  Right () -> expectationFailure [i|Couldn't find file in '#{dir}'|]
+  Right () -> expectationFailure [i|Couldn't find file matching predicate in '#{dirs}'|]
   where
     go :: FilePath -> ExceptT FilePath IO ()
     go currentDir = do
diff --git a/lib/Test/Sandwich/Contexts/HttpWaits.hs b/lib/Test/Sandwich/Contexts/HttpWaits.hs
--- a/lib/Test/Sandwich/Contexts/HttpWaits.hs
+++ b/lib/Test/Sandwich/Contexts/HttpWaits.hs
@@ -21,7 +21,6 @@
 
 import Control.Concurrent
 import Control.Monad
-import Control.Monad.Catch (MonadThrow)
 import Control.Monad.IO.Unlift
 import Control.Monad.Logger
 import Data.Maybe
@@ -46,7 +45,7 @@
 timePerRequest :: Int
 timePerRequest = 10_000_000
 
-type WaitConstraints m = (HasCallStack, MonadLogger m, MonadUnliftIO m, MonadThrow m)
+type WaitConstraints m = (HasCallStack, MonadLogger m, MonadUnliftIO m)
 
 -- | Whether to verify certificates or not when connecting to an HTTPS endpoint.
 data VerifyCerts = YesVerify | NoVerify
@@ -70,7 +69,9 @@
 waitUntilStatusCode :: (WaitConstraints m) => (Int, Int, Int) -> VerifyCerts -> String -> m ()
 waitUntilStatusCode code verifyCerts url = do
   debug [i|Beginning waitUntilStatusCode request to #{url}|]
-  req <- parseRequest url
+  req <- case parseRequest url of
+    Left err -> expectationFailure [i|Couldn't parse URL: #{url}: #{err}|]
+    Right x -> pure x
   man <- liftIO $ newManager (if verifyCerts == YesVerify then tlsManagerSettings else tlsNoVerifySettings)
   timeout timePerRequest (handleException $ (Right <$>) $ httpLbs req man) >>= \case
     Just (Right resp)
@@ -95,12 +96,3 @@
   maybeSuccess <- timeout timeInMicroseconds $ waitUntilStatusCode code verifyCerts url
   when (isNothing maybeSuccess) $
     expectationFailure [i|Failed to connect to URL "#{url}" in waitUntilStatusCodeWithTimeout'...|]
-
-
-#if !MIN_VERSION_time(1,9,1)
-secondsToNominalDiffTime :: Pico -> NominalDiffTime
-secondsToNominalDiffTime = realToFrac
-
-nominalDiffTimeToSeconds :: NominalDiffTime -> Pico
-nominalDiffTimeToSeconds = realToFrac
-#endif
diff --git a/lib/Test/Sandwich/Contexts/Nix.hs b/lib/Test/Sandwich/Contexts/Nix.hs
--- a/lib/Test/Sandwich/Contexts/Nix.hs
+++ b/lib/Test/Sandwich/Contexts/Nix.hs
@@ -37,6 +37,8 @@
   -- * Nix environments
   , introduceNixEnvironment
   , introduceNixEnvironment'
+  , buildNixPackage
+  , buildNixPackage'
   , buildNixSymlinkJoin
   , buildNixSymlinkJoin'
   , buildNixExpression
@@ -46,6 +48,9 @@
 
   -- * Nixpkgs releases #releases#
   , nixpkgsReleaseDefault
+  , nixpkgsMaster
+  , nixpkgsRelease2505
+  , nixpkgsRelease2411
   , nixpkgsRelease2405
   , nixpkgsRelease2311
 
@@ -62,7 +67,7 @@
   , defaultFileContextVisibilityThreshold
   ) where
 
-import Control.Monad.Catch (MonadMask, MonadThrow)
+import Control.Monad.Catch (MonadThrow)
 import Control.Monad.IO.Unlift
 import Control.Monad.Logger
 import Data.Aeson as A
@@ -73,7 +78,7 @@
 import qualified Data.Vector as V
 import Relude
 import System.FilePath
-import System.IO.Temp
+import System.IO.Temp (createTempDirectory)
 import Test.Sandwich
 import Test.Sandwich.Contexts.Files.Types
 import Test.Sandwich.Contexts.Util.Aeson
@@ -83,6 +88,7 @@
 import UnliftIO.Environment
 import UnliftIO.MVar (modifyMVar)
 import UnliftIO.Process
+import UnliftIO.Temporary
 
 -- * Types
 
@@ -119,6 +125,42 @@
     , nixpkgsDerivationAllowUnfree :: Bool
     } deriving (Show, Eq)
 
+-- | Nixpkgs master, accessed 6\/6\/2025.
+-- You can compute updated values for this release (or others) by running
+-- nix-prefetch-github NixOS nixpkgs --rev master
+nixpkgsMaster :: NixpkgsDerivation
+nixpkgsMaster = NixpkgsDerivationFetchFromGitHub {
+  nixpkgsDerivationOwner = "NixOS"
+  , nixpkgsDerivationRepo = "nixpkgs"
+  , nixpkgsDerivationRev = "067a39e41a125985e061199452c900b0305f4c42"
+  , nixpkgsDerivationSha256 = "sha256-N57WqGFUUDJ7QVR4YPRttp4YuTA4oN/KdXHY4OEXGFk="
+  , nixpkgsDerivationAllowUnfree = False
+  }
+
+-- | Nixpkgs release 25.05, accessed 6\/6\/2025.
+-- You can compute updated values for this release (or others) by running
+-- nix-prefetch-github NixOS nixpkgs --rev release-25.05
+nixpkgsRelease2505 :: NixpkgsDerivation
+nixpkgsRelease2505 = NixpkgsDerivationFetchFromGitHub {
+  nixpkgsDerivationOwner = "NixOS"
+  , nixpkgsDerivationRepo = "nixpkgs"
+  , nixpkgsDerivationRev = "8217c6edf391991f07ecacf3d31ba6eb01d733b1"
+  , nixpkgsDerivationSha256 = "sha256-aaeXPG9zVvi+aKTp0dMUYOeMuhDXQejRPh2CfK23nf8="
+  , nixpkgsDerivationAllowUnfree = False
+  }
+
+-- | Nixpkgs release 24.11, accessed 6\/6\/2025.
+-- You can compute updated values for this release (or others) by running
+-- nix-prefetch-github NixOS nixpkgs --rev release-24.11
+nixpkgsRelease2411 :: NixpkgsDerivation
+nixpkgsRelease2411 = NixpkgsDerivationFetchFromGitHub {
+  nixpkgsDerivationOwner = "NixOS"
+  , nixpkgsDerivationRepo = "nixpkgs"
+  , nixpkgsDerivationRev = "5908ad2494520214a309e74d5c3f33623a593ecd"
+  , nixpkgsDerivationSha256 = "sha256-0q80SLtfhrtZAzLGpwAQjqaTE+HAwmOjoX4Q3M5mB/s="
+  , nixpkgsDerivationAllowUnfree = False
+  }
+
 -- | Nixpkgs release 24.05, accessed 11\/9\/2024.
 -- You can compute updated values for this release (or others) by running
 -- nix-prefetch-github NixOS nixpkgs --rev release-24.05
@@ -238,6 +280,32 @@
   -> SpecFree context m ()
 introduceNixEnvironment' nodeOptions packageNames = introduce' nodeOptions "Introduce Nix environment" nixEnvironment (buildNixSymlinkJoin packageNames) (const $ return ())
 
+-- | Build a single Nix package name from Nixpkgs
+buildNixPackage :: (
+  HasBaseContextMonad context m, HasNixContext context
+  , MonadUnliftIO m, MonadLogger m
+  )
+  -- | Package name.
+  => Text
+  -> m FilePath
+buildNixPackage packageName = do
+  nc <- getContext nixContext
+  buildNixPackage' nc packageName
+
+-- | Lower-level version of 'buildNixCallPackageDerivation'
+buildNixPackage' :: (
+  HasBaseContextMonad context m
+  , MonadUnliftIO m, MonadLogger m
+  )
+  -- | Nix context.
+  => NixContext
+  -- | Package name.
+  -> Text
+  -> m FilePath
+buildNixPackage' nc packageName = buildNixCallPackageDerivation' nc expr
+  where
+    expr = [i|{ pkgs }: pkgs."#{packageName}"|]
+
 -- | Build a Nix environment, as in 'introduceNixEnvironment'.
 buildNixSymlinkJoin :: (
   HasBaseContextMonad context m, HasNixContext context
@@ -267,7 +335,7 @@
 -- "{ git, gcc, stdenv, ... }: stdenv.mkDerivation {...}"
 buildNixCallPackageDerivation :: (
   HasBaseContextMonad context m, HasNixContext context
-  , MonadUnliftIO m, MonadLogger m, MonadMask m
+  , MonadUnliftIO m, MonadLogger m
   )
   -- | Nix derivation
   => Text
@@ -277,9 +345,9 @@
   buildNixCallPackageDerivation' nc derivation
 
 -- | Lower-level version of 'buildNixCallPackageDerivation'
-buildNixCallPackageDerivation' :: (
+buildNixCallPackageDerivation' :: forall context m. (
   HasBaseContextMonad context m
-  , MonadUnliftIO m, MonadLogger m, MonadMask m
+  , MonadUnliftIO m, MonadLogger m
   )
   -- | Nix context.
   => NixContext
@@ -303,6 +371,7 @@
         return (M.insert derivation asy m, asy)
     )
   where
+    withDerivationPath :: Maybe FilePath -> (FilePath -> m a) -> m a
     withDerivationPath (Just nixExpressionDir) action = action (nixExpressionDir </> "default.nix")
     withDerivationPath Nothing action = withSystemTempDirectory "nix-expression" $ \dir -> action (dir </> "default.nix")
 
@@ -359,7 +428,7 @@
                  , "--extra-experimental-features", "nix-command"
                  , "--expr", toString expr
                  , "--json"
-                 ] <> (case maybeOutputPath of Nothing -> []; Just p -> ["-o", p])
+                 ] <> (case maybeOutputPath of Nothing -> ["--no-link"]; Just p -> ["-o", p])
                 )) { env = maybeEnv }
     ) ""
 
@@ -390,8 +459,8 @@
 pkgs.symlinkJoin { name = "test-contexts-environment"; paths = with pkgs; [#{T.intercalate " " packageNames}]; }
 |]
 
-renderCallPackageDerivation :: NixpkgsDerivation -> FilePath -> Text
-renderCallPackageDerivation (NixpkgsDerivationFetchFromGitHub {..}) derivationPath = [i|
+renderDerivationWithPkgs :: NixpkgsDerivation -> Text -> Text
+renderDerivationWithPkgs (NixpkgsDerivationFetchFromGitHub {..}) expr = [i|
 \# Use the ambient <nixpkgs> channel to bootstrap
 with {
   inherit (import (<nixpkgs>) {})
@@ -409,5 +478,11 @@
   pkgs = import nixpkgs {};
 in
 
-pkgs.callPackage #{show derivationPath :: String} {}
+#{expr}
 |]
+
+renderCallPackageDerivation :: NixpkgsDerivation -> FilePath -> Text
+renderCallPackageDerivation nixpkgsDerivation derivationPath =
+  renderDerivationWithPkgs nixpkgsDerivation expr
+  where
+    expr = [i|pkgs.callPackage #{show derivationPath :: String} {}|]
diff --git a/lib/Test/Sandwich/Contexts/PostgreSQL.hs b/lib/Test/Sandwich/Contexts/PostgreSQL.hs
--- a/lib/Test/Sandwich/Contexts/PostgreSQL.hs
+++ b/lib/Test/Sandwich/Contexts/PostgreSQL.hs
@@ -205,7 +205,7 @@
   -> (FilePath -> m a)
   -> m a
 withPostgresUnixSocketViaNix (PostgresNixOptions {..}) action = do
-  postgresBinDir <- (</> "bin") <$> buildNixSymlinkJoin [postgresNixPostgres]
+  postgresBinDir <- (</> "bin") <$> buildNixPackage postgresNixPostgres
   withPostgresUnixSocket postgresBinDir postgresNixUsername postgresNixPassword postgresNixDatabase postgresNixConfExtraLines action
 
 -- | Lower-level variant of 'withPostgresUnixSocket'.
@@ -219,7 +219,7 @@
   -> (FilePath -> m a)
   -> m a
 withPostgresUnixSocketViaNix' nc (PostgresNixOptions {..}) action = do
-  postgresBinDir <- (</> "bin") <$> buildNixSymlinkJoin' nc [postgresNixPostgres]
+  postgresBinDir <- (</> "bin") <$> buildNixPackage' nc postgresNixPostgres
   withPostgresUnixSocket postgresBinDir postgresNixUsername postgresNixPassword postgresNixDatabase postgresNixConfExtraLines action
 
 -- | The lowest-level raw process version.
diff --git a/lib/Test/Sandwich/Contexts/ReverseProxy/TCP.hs b/lib/Test/Sandwich/Contexts/ReverseProxy/TCP.hs
--- a/lib/Test/Sandwich/Contexts/ReverseProxy/TCP.hs
+++ b/lib/Test/Sandwich/Contexts/ReverseProxy/TCP.hs
@@ -17,6 +17,7 @@
 import Test.Sandwich (expectationFailure)
 import UnliftIO.Async
 import UnliftIO.Exception
+import UnliftIO.Timeout
 
 
 withProxyToUnixSocket :: MonadUnliftIO m => FilePath -> (PortNumber -> m a) -> m a
@@ -29,8 +30,11 @@
                SockAddrInet6 port _ _ _ -> putMVar portVar port
                x -> expectationFailure [i|withProxyToUnixSocket: expected to bind a TCP socket, but got other addr: #{x}|]
            )
-  withAsync (liftIO $ DCN.runTCPServer ss app `onException` (putMVar portVar 0)) $ \_ ->
-    readMVar portVar >>= f
+  withAsync (liftIO $ DCN.runTCPServer ss app `onException` (tryPutMVar portVar (-1))) $ \_ ->
+    timeout 60_000_000 (readMVar portVar) >>= \case
+      Nothing -> expectationFailure [i|withProxyToUnixSocket: didn't get port within 60s|]
+      Just (-1) -> expectationFailure [i|withProxyToUnixSocket: TCP server threw exception|]
+      Just port -> f port
 
   where
     app appdata = DCNU.runUnixClient (DCNU.clientSettings socketPath) $ \appdataServer ->
diff --git a/lib/Test/Sandwich/Contexts/Util/Nix.hs b/lib/Test/Sandwich/Contexts/Util/Nix.hs
--- a/lib/Test/Sandwich/Contexts/Util/Nix.hs
+++ b/lib/Test/Sandwich/Contexts/Util/Nix.hs
@@ -3,18 +3,18 @@
   withWritableBinaryCache
   ) where
 
-import Control.Monad.Catch (MonadMask)
+import Control.Monad.IO.Unlift
 import Control.Monad.Logger
 import Data.String.Interpolate
 import Relude
 import System.FilePath
-import System.IO.Temp
 import Test.Sandwich.Logging
 import UnliftIO.Directory
 import UnliftIO.Process
+import UnliftIO.Temporary
 
 
-withWritableBinaryCache :: (MonadIO m, MonadMask m, MonadLogger m) => Maybe FilePath -> (Maybe FilePath -> m a) -> m a
+withWritableBinaryCache :: (MonadUnliftIO m, MonadLogger m) => Maybe FilePath -> (Maybe FilePath -> m a) -> m a
 withWritableBinaryCache Nothing action = action Nothing
 withWritableBinaryCache (Just readOnlyPath) action =
   withSystemTempDirectory "writable-binary-cache" $ \dir -> do
diff --git a/sandwich-contexts.cabal b/sandwich-contexts.cabal
--- a/sandwich-contexts.cabal
+++ b/sandwich-contexts.cabal
@@ -1,11 +1,11 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.37.0.
+-- This file has been generated from package.yaml by hpack version 0.38.0.
 --
 -- see: https://github.com/sol/hpack
 
 name:           sandwich-contexts
-version:        0.3.0.2
+version:        0.3.0.3
 synopsis:       Contexts for the Sandwich test library
 description:    Please see the <https://codedownio.github.io/sandwich documentation>.
 author:         Tom McLaughlin
@@ -59,6 +59,8 @@
     , conduit
     , conduit-extra >=1.3.7
     , containers
+    , crypton-connection
+    , data-default
     , exceptions
     , filepath
     , http-client
@@ -77,20 +79,12 @@
     , string-interpolate
     , temporary
     , text
-    , time
     , transformers
     , unix-compat
     , unliftio
     , unliftio-core
     , vector
   default-language: Haskell2010
-  if impl(ghc >= 9.6)
-    build-depends:
-        crypton-connection
-      , data-default
-  if impl(ghc < 9.6)
-    build-depends:
-        connection
 
 test-suite tests
   type: exitcode-stdio-1.0
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -18,7 +18,6 @@
 -- testsPooled = $(getSpecFromFolder $ defaultGetSpecFromFolderOptions {
 --   getSpecCombiner = 'describeParallel
 --   , getSpecIndividualSpecHooks = 'poolify
---   , getSpecWarnOnParseError = NoWarnOnParseError
 --   })
 
 -- main :: IO ()
