<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Filis Futsarov</title>
    <description>The latest articles on DEV Community by Filis Futsarov (@filisko).</description>
    <link>https://dev.to/filisko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F171104%2Ff2fd4bbb-41d0-484e-bf3b-16e1a20c5919.jpeg</url>
      <title>DEV Community: Filis Futsarov</title>
      <link>https://dev.to/filisko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/filisko"/>
    <language>en</language>
    <item>
      <title>How to set up Database Integration Tests in vanilla PHP</title>
      <dc:creator>Filis Futsarov</dc:creator>
      <pubDate>Sat, 29 Nov 2025 16:37:00 +0000</pubDate>
      <link>https://dev.to/filisko/how-to-set-up-database-integration-tests-in-vanilla-php-ge9</link>
      <guid>https://dev.to/filisko/how-to-set-up-database-integration-tests-in-vanilla-php-ge9</guid>
      <description>&lt;p&gt;In this guide I’ll show you how to run fast, isolated, high-quality Database Integration Tests in legacy or framework-less PHP projects. Only Doctrine or PDO needed, and a small but incredibly powerful trick used by many battle-tested frameworks across different programming languages ecosystems.&lt;/p&gt;

&lt;p&gt;One reason this is a very solid approach is that it provides the guarantees of real database integration tests — transactions, persisted data, and SQL queries hitting a real database — while keeping execution times extremely low. This makes it ideal for large test suites, continuous refactoring, and yes, even TDD, because it preserves your development flow through a fast feedback loop.&lt;/p&gt;

&lt;p&gt;Also, this approach works exceptionally well in legacy projects. Most legacy codebases lack a Testing Foundation. With this technique, you can introduce high-level database integration tests even into very old or badly coupled systems.&lt;/p&gt;

&lt;p&gt;Please note that before going 'all-in' into this approach, I've tried different alternatives, here are two of them:&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ &lt;strong&gt;SQLite's In-Memory Database support&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;SQLite can run entirely in memory, meaning you can have a fully isolated database instance that lives only in RAM.&lt;/p&gt;

&lt;p&gt;For example, you could run your Database Integration Tests across 16 parallel processes, each with its own in-memory database.&lt;/p&gt;

&lt;p&gt;This is EXTREMELY fast — and a perfectly valid approach if SQLite is your primary database — but there’s a significant gap between SQLite and, for example, PostgreSQL. In behaviour, data types, and SQL semantics. MySQL is somewhat closer to SQLite, but still not equivalent.&lt;/p&gt;

&lt;p&gt;If your main database is other than SQLite and you choose this approach, you’ll need to limit your queries to the subset of features SQLite supports. And even then, there will always be a non-negligible mismatch, which may keep your confidence from reaching 100%.&lt;/p&gt;

&lt;p&gt;In PHP, you can set up SQLite's in-memory Database like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sqlite::memory:'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just be aware that with any in-memory database testing setup, you’ll need to recreate the schema for every run, as nothing is persisted.&lt;/p&gt;

&lt;p&gt;I suggest that you take a look at the official documentation for more details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/ref.pdo-sqlite.connection.php" rel="noopener noreferrer"&gt;php.net - PDO SQLite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sqlite.org/inmemorydb.html" rel="noopener noreferrer"&gt;sqlite.org - In-Memory database&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ❌ &lt;strong&gt;Vimeo's In-Memory MySQL engine&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I was genuinely surprised when I came across this project. And the good part is that I can speak from experience, having used it for a couple of months. Vimeo describes it as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A MySQL engine written in pure PHP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To quickly illustrate the main idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// use a class specific to your current PHP version (APIs changed in major versions)&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\Vimeo\MysqlEngine\Php8\FakePdo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// currently supported attributes&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;\PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_CASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;\PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CASE_LOWER&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;\PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_EMULATE_PREPARES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library provides its own PDO implementation, which effectively acts as the interface to Vimeo’s MySQL engine under the hood. In theory, it works the same way as a regular PDO instance, and you can generally use it anywhere you would use native PDO.&lt;/p&gt;

&lt;p&gt;Some issues I've noticed, though:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limited capabilities.&lt;/strong&gt; Its feature set is much smaller than native PDO. Only a few of the most common PDO attributes are supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unclear error messages.&lt;/strong&gt; The library throws PHP-level exceptions instead of real SQL errors, and they’re often not very intuitive. For example, an SQL syntax error or assigning &lt;code&gt;NULL&lt;/code&gt; to a non-nullable column produces a library-generated exception rather than the usual MySQL error message — and these can be confusing at first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning curve.&lt;/strong&gt; It takes some time to become familiar with its error patterns before you can be truly productive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL only.&lt;/strong&gt; It only supports MySQL type-of databases, so it’s not suitable if your main database is PostgreSQL, SQLite, or anything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the other hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It’s extremely fast&lt;/strong&gt; — there’s no network or database server involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It requires no infrastructure changes&lt;/strong&gt; — you just need to recreate the schema each time since everything lives in memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It’s reliable&lt;/strong&gt; — having been used in production for years and backed by Vimeo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I recommend you to take a deeper look and see their real motivation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/vimeo/php-mysql-engine?tab=readme-ov-file#motivation" rel="noopener noreferrer"&gt;GitHub - vimeo/php-mysql-engine&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  ✅ Transactional Database Integration Tests
&lt;/h1&gt;

&lt;p&gt;This is the chosen approach of this guide.&lt;/p&gt;

&lt;p&gt;As you may have noticed, all the previous options come with significant limitations. Unless you choose the SQLite in-memory approach and SQLite is your primary database, none of them provides a 100% trustworthy integration test.&lt;/p&gt;

&lt;p&gt;The only way to guarantee fully reliable tests is to interact with your database — the same your application uses. This approach does exactly that and works with any database system that supports transactions.&lt;/p&gt;

&lt;p&gt;The idea behind this technique is surprisingly simple. At its core, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_user_registration&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// start transaction&lt;/span&gt;
    &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;beginTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// interact with the database performing real operations&lt;/span&gt;
    &lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'INSERT INTO users (name) VALUES (:name)'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="c1"&gt;// the user was inserted, we can do some assertions&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT * FROM users'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchAll&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// our test has finished, we roll back everything&lt;/span&gt;
    &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;rollBack&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Starts a database transaction,&lt;/li&gt;
&lt;li&gt;performs a real insert,&lt;/li&gt;
&lt;li&gt;gets a real auto-incremented ID,&lt;/li&gt;
&lt;li&gt;and finally rolls everything back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is real database interaction with almost zero side effects — and, most importantly, enables a fast and reliable feedback loop that keeps your development flow smooth. The only noticeable side effect that I realized is that if you use auto-incremented IDs they will keep increasing.&lt;/p&gt;

&lt;p&gt;Modern testing setups wrap &lt;code&gt;beginTransaction()&lt;/code&gt; and &lt;code&gt;rollBack()&lt;/code&gt; inside methods such as &lt;code&gt;setUp()&lt;/code&gt; and &lt;code&gt;tearDown()&lt;/code&gt; which are specific to the Testing Frameworks, in this case, PHPUnit. But the underlying mechanism is exactly the same.&lt;/p&gt;

&lt;p&gt;Also, you’ll probably want to separate your testing and development databases. If you mix them (use you development database for tests), your tests won’t start from a clean state, and you’ll eventually end up with incorrect assumptions and unreliable results.&lt;/p&gt;

&lt;h1&gt;
  
  
  Who uses this approach?
&lt;/h1&gt;

&lt;p&gt;This technique is not new. In fact, it’s well-established and widely used across many battle-tested frameworks and tools in both PHP and non-PHP ecosystems. Frameworks like Ruby on Rails, Django (Python) and Spring Boot (Java) rely on the same idea: run each test inside a database transaction and roll it back at the end.&lt;/p&gt;

&lt;p&gt;Over the years, this pattern has proven to be one of the fastest, cleanest, and most reliable ways to write real database integration tests.&lt;/p&gt;

&lt;p&gt;Here are some well-known examples:&lt;/p&gt;

&lt;h2&gt;
  
  
  Ruby On Rails
&lt;/h2&gt;

&lt;p&gt;Since the early versions of Rails (2005–2006, around its initial release), this mechanism has been supported.&lt;/p&gt;

&lt;p&gt;This approach allowed Rails applications to scale their test suites without suffering the performance penalties of repeatedly creating or truncating tables, and it helped popularize transactional testing patterns in many other frameworks.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;By default, Rails automatically wraps tests in a database transaction that is rolled back once completed. This makes tests independent of each other and means that changes to the database are only visible within a single test.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; &lt;a href="https://guides.rubyonrails.org/testing.html#transactions" rel="noopener noreferrer"&gt;Ruby on Rails - Transactional Database Tests&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  WordPress
&lt;/h2&gt;

&lt;p&gt;Since 2017, WordPress’ PHPUnit test suite has adopted this transactional approach: each test starts a MySQL transaction and rolls it back after execution. This ensures real SQL behavior while keeping the database clean between tests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Database modifications made during test, on the other hand, are not persistent. Before each test, the suite opens a MySQL transaction (&lt;code&gt;START TRANSACTION&lt;/code&gt;) with autocommit disabled, &lt;strong&gt;and at the end of each test the transaction is rolled back&lt;/strong&gt; (&lt;code&gt;ROLLBACK&lt;/code&gt;). This means that database operations performed from within a test, such as the creation of test fixtures, are discarded after each test.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; &lt;a href="https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/#database" rel="noopener noreferrer"&gt;WordPress Handbook - Testing with PHPUnit&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Illuminate\Foundation\Testing\RefreshDatabase&lt;/code&gt; Trait in Laravel also does exactly what we described. It wraps the test within a Database transaction, and rolls back everything at the end of the test.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;Illuminate\Foundation\Testing\RefreshDatabase&lt;/code&gt; trait does not migrate your database if your schema is up to date. Instead, &lt;strong&gt;it will only execute the test within a database transaction&lt;/strong&gt;. Therefore, any records added to the database by test cases that do not use this trait may still exist in the database.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; &lt;a href="https://laravel.com/docs/11.x/database-testing#resetting-the-database-after-each-test" rel="noopener noreferrer"&gt;Laravel - Resetting the Database after each test&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Symfony
&lt;/h2&gt;

&lt;p&gt;In the Symfony ecosystem, this approach is commonly implemented through the &lt;code&gt;dama/doctrine-test-bundle&lt;/code&gt;, a bundle that — as of today — has more than 33 million downloads.&lt;/p&gt;

&lt;p&gt;It is also one of the most decoupled, enterprise-grade solutions available. In practice, this means you can use the 'non-Symfony' part of this library in virtually any project, benefiting from the  level of robustness and reliability that it has gained over the years.&lt;/p&gt;

&lt;p&gt;You might hesitate about the Doctrine requirement — but there’s an important reason for it. This whole approach relies on database transactions, and that raises an immediate question: &lt;strong&gt;what happens if your application performs nested transactions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is exactly where the library shines. It handles transactional tests, even when your code opens its own transactions internally. Thanks to Doctrine’s DBAL middleware and its savepoint support, nested transactions work seamlessly on drivers such as PostgreSQL and MySQL.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reference: &lt;a href="https://github.com/dmaicher/doctrine-test-bundle" rel="noopener noreferrer"&gt;GitHub - dmaicher/doctrine-test-bundle&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  How to setup &lt;code&gt;dmaicher/doctrine-test-bundle&lt;/code&gt; in your framework-agnostic project
&lt;/h1&gt;

&lt;p&gt;In this section, we’ll focus on how to configure this library for any framework-agnostic project — whether it’s a legacy codebase or a modern project where you intentionally chose to keep things minimal.&lt;/p&gt;

&lt;p&gt;As a matter of fact, &lt;a href="https://github.com/dmaicher/doctrine-test-bundle/issues/318" rel="noopener noreferrer"&gt;I actually posted a question in library’s GitHub repository&lt;/a&gt; asking about this exact use case, and David Maicher, the official maintainer, was kind enough to help me through the details. What follows is essentially the result of that exchange.&lt;/p&gt;

&lt;p&gt;This assumes you already have a working Doctrine connection in place.&lt;/p&gt;

&lt;p&gt;Install the composer package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require dama/doctrine-test-bundle:^8.4 &lt;span class="nt"&gt;--dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Add library's PHPUnit extension to &lt;code&gt;phpunit.xml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is required so that PHPUnit automatically rolls back the database transaction after each test.&lt;/p&gt;

&lt;p&gt;Add the following &lt;code&gt;&amp;lt;extensions&amp;gt;&lt;/code&gt; block — or simply add the &lt;code&gt;&amp;lt;bootstrap class&amp;gt;&lt;/code&gt; to your existing &lt;code&gt;&amp;lt;extensions&amp;gt;&lt;/code&gt; section if you already have one — to your &lt;code&gt;phpunit.xml&lt;/code&gt; (or the one you use) file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;phpunit&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- ...other stuff ... --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;extensions&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;bootstrap&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/extensions&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/phpunit&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Set up the Doctrine connection using the library’s components
&lt;/h2&gt;

&lt;p&gt;The following code shows what you would normally want to have in your “Doctrine connection” setup.&lt;/p&gt;

&lt;p&gt;The key parts are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;dama.connection_key&lt;/code&gt; parameter (it can be set to anything, but it must be present)&lt;/li&gt;
&lt;li&gt;adding the bundle’s DBAL Middleware&lt;/li&gt;
&lt;li&gt;calling &lt;code&gt;setKeepStaticConnections(true)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you miss any of these, the integration test won't work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Doctrine\ORM\Configuration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Doctrine\ORM\ORMSetup&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Doctrine\DBAL\Connection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Doctrine\DBAL\DriverManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getDoctrineConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Environment&lt;/span&gt; &lt;span class="nv"&gt;$environment&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Connection&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$parameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'driver'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'pdo_pgsql'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'user'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'postgresql'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'postgresql'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'dbname'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'app_prod'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// this is not relevant for this example, but if you use Doctrine, you probably use the ORM too.&lt;/span&gt;
    &lt;span class="c1"&gt;// And this is just to make it more similar to your context.&lt;/span&gt;
    &lt;span class="nv"&gt;$config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ORMSetup&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;createAttributeMetadataConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$domainEntitiesPath&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;isDevMode&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$environment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;production&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// you will probably want to have a check similar to this&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$environment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// also, you probably want to switch to an empty, different database for testing!&lt;/span&gt;
        &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'dbname'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'app_tests'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// set a connection key&lt;/span&gt;
        &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'dama.connection_key'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'anything-is-ok'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="c1"&gt;// add the DBAL middleware&lt;/span&gt;
        &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setMiddlewares&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\DAMA\DoctrineTestBundle\Doctrine\DBAL\Middleware&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="c1"&gt;// keep static connections across tests&lt;/span&gt;
        &lt;span class="nc"&gt;\DAMA\DoctrineTestBundle\Doctrine\DBAL\StaticDriver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;setKeepStaticConnections&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DriverManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is it. You don’t need anything else.&lt;/p&gt;

&lt;p&gt;This is a complete example of how a Database Integration Test now looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;PHPUnit\Framework\TestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SomeTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_doctrine_connection&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getDoctrineConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Insert a real row into the database&lt;/span&gt;
        &lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="c1"&gt;// Fetch the last inserted ID&lt;/span&gt;
        &lt;span class="nv"&gt;$userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;lastInsertId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Verify the row exists&lt;/span&gt;
        &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'SELECT name FROM users WHERE id = :id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// No cleanup needed — everything will be rolled back automatically&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hope this post helped you understand how to perform real database integration tests in PHP without relying on any framework.&lt;/p&gt;

&lt;h1&gt;
  
  
  Share your thoughts
&lt;/h1&gt;

&lt;p&gt;Did you find this approach useful? Would you like to hear about other variations?&lt;/p&gt;

&lt;p&gt;If you tried it or ran into anything unexpected, I'd be happy hear how it went — your experience helps keep this post accurate and helpful for others.&lt;/p&gt;

&lt;p&gt;And if you’re applying this technique in a real project — especially a legacy one — feel free to share your story. This can encourage others to adopt it as well.&lt;/p&gt;

&lt;p&gt;Special thanks to &lt;a href="https://github.com/dmaicher" rel="noopener noreferrer"&gt;David Maicher (@dmaicher)&lt;/a&gt;, the maintainer of the &lt;code&gt;doctrine-test-bundle&lt;/code&gt; project, for helping clarify how to use the library in a framework-agnostic context.&lt;/p&gt;

</description>
      <category>php</category>
      <category>legacy</category>
      <category>testing</category>
    </item>
    <item>
      <title>How to automatically decrypt a LUKS LVM setup on boot with a USB</title>
      <dc:creator>Filis Futsarov</dc:creator>
      <pubDate>Wed, 25 Jun 2025 10:07:01 +0000</pubDate>
      <link>https://dev.to/filisko/how-to-automatically-decrypt-a-luks-lvm-setup-on-boot-with-a-usb-411i</link>
      <guid>https://dev.to/filisko/how-to-automatically-decrypt-a-luks-lvm-setup-on-boot-with-a-usb-411i</guid>
      <description>&lt;p&gt;This guide walks you through a robust procedure to auto-decrypt a LUKS-on-LVM setup at boot with a USB key. It assumes you already have your system set up with LUKS encryption on LVM.&lt;/p&gt;

&lt;p&gt;The core idea is simple: keep a dedicated USB stick at home to unlock your system effortlessly, and leave it behind whenever you head out, so your machine stays securely encrypted on the go.&lt;/p&gt;

&lt;p&gt;I extensively tested the procedure in multiple VMs and on real-world installs of Ubuntu 24.04 and Pop!_OS 22.04. Until reaching that point, I repeatedly broke my VM while trying outdated guides, AI-generated suggestions, and other unreliable sources—until I found my own way and then passed it to real setups. Here’s what I tried (and what finally worked with some customization):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ initramfs hooks&lt;/li&gt;
&lt;li&gt;❌ udev scripts&lt;/li&gt;
&lt;li&gt;❌ dracut modules&lt;/li&gt;
&lt;li&gt;✅ cryptsetup using &lt;code&gt;keyscript&lt;/code&gt; option (handled by &lt;code&gt;initramfs-tools&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This solution relies on the &lt;code&gt;keyscript&lt;/code&gt; option supported by &lt;code&gt;initramfs-tools&lt;/code&gt; (not by &lt;code&gt;cryptsetup&lt;/code&gt; itself), which allows delegating the decryption process to a custom script in a higher-level and safer way than using low-level &lt;code&gt;initramfs&lt;/code&gt; hooks — although the option is defined in &lt;code&gt;/etc/crypttab&lt;/code&gt; and passed to the initramfs layer, &lt;code&gt;cryptsetup&lt;/code&gt; itself only accepts raw key data.&lt;/p&gt;

&lt;p&gt;Internally, &lt;code&gt;initramfs-tools&lt;/code&gt; handles &lt;code&gt;keyscript&lt;/code&gt; by executing the specified script and piping its output to &lt;code&gt;cryptsetup&lt;/code&gt;, effectively feeding the keyfile through standard input.&lt;/p&gt;

&lt;p&gt;Most scripts I came across failed to handle this process in a fault-tolerant way: instead of falling back to the familiar "Enter password" screen when any kind of error happens, they leave you with a broken system.&lt;/p&gt;

&lt;p&gt;That’s why I wrote a script that does things properly. It mounts your USB, attempts to load the keyfile up to three times, and if it still fails, gracefully falls back to the standard password prompt you're used to.&lt;/p&gt;

&lt;p&gt;Before proceeding, ⚠️ &lt;strong&gt;make a full disk backup using Clonezilla (not just the partition)&lt;/strong&gt; and &lt;strong&gt;test this solution in a VM&lt;/strong&gt; running the same Ubuntu version as yours. This ensures you're familiar with the process and minimizes risk — I personally broke my VM several times during testing for writing this post, but snapshots made rollback easy.&lt;/p&gt;

&lt;p&gt;The only requirement for this solution to work is support for the &lt;code&gt;keyscript&lt;/code&gt; parameter, which has been available in &lt;code&gt;initramfs-tools&lt;/code&gt; since at least Ubuntu 16.04.&lt;/p&gt;

&lt;p&gt;✅ Tested on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu 22.04 and 24.04
&lt;/li&gt;
&lt;li&gt;Pop!_OS 22.04&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These cover &lt;code&gt;cryptsetup&lt;/code&gt; versions from &lt;strong&gt;2.4.3&lt;/strong&gt; to &lt;strong&gt;2.7.0&lt;/strong&gt;. You can verify your version with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cryptsetup &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Creating a secure keyfile
&lt;/h2&gt;

&lt;p&gt;This command creates a 4MB keyfile filled with cryptographically secure binary data (non-ASCII characters), providing high entropy per byte — making brute-force attacks significantly harder.&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;Tip&lt;/strong&gt;: Save the keyfile directly to the USB drive that you'll use to decrypt your setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/urandom &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/media/usb/secure.key &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="nv"&gt;iflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fullblock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Downloading the unlock script
&lt;/h2&gt;

&lt;p&gt;We'll need the unlock script for the &lt;code&gt;keyscript&lt;/code&gt; parameter in the &lt;code&gt;crypttab&lt;/code&gt; entry.&lt;/p&gt;

&lt;p&gt;In systemd-based setups (e.g. &lt;strong&gt;Pop!_OS&lt;/strong&gt;), the keyscript must be placed inside &lt;code&gt;/lib/cryptsetup/scripts&lt;/code&gt;. In non-systemd setups (e.g. &lt;strong&gt;Ubuntu&lt;/strong&gt;), it can technically reside anywhere — but placing it in &lt;code&gt;/lib/cryptsetup/scripts&lt;/code&gt; ensures compatibility across systems.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;wget &lt;span class="nt"&gt;-O&lt;/span&gt; /lib/cryptsetup/scripts/keyscript.sh &lt;span class="se"&gt;\&lt;/span&gt;
  https://raw.githubusercontent.com/filisko/cryptsetup-usb-keyscript/main/src/keyscript.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ &lt;strong&gt;It's critical&lt;/strong&gt; that the script is owned by root and has execution permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /lib/cryptsetup/scripts/keyscript.sh
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;755 /lib/cryptsetup/scripts/keyscript.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Finding your USB's UUID
&lt;/h2&gt;

&lt;p&gt;We need USB's UUID to tell &lt;code&gt;cryptsetup&lt;/code&gt; where to find the keyfile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;blkid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There will be many lines like the following one. We're interested in the UUID="81D6-413D" part.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dev/sda1: UUID="81D6-413D" BLOCK_SIZE="512" TYPE="vfat" PARTUUID="c371356e-01"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Updating &lt;code&gt;/etc/crypttab&lt;/code&gt; entries
&lt;/h2&gt;

&lt;p&gt;To enable automatic decryption, we need to edit &lt;code&gt;/etc/crypttab&lt;/code&gt; and provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The LUKS device name (already there).&lt;/li&gt;
&lt;li&gt;The UUID of the LUKS-encrypted root partition (already there).&lt;/li&gt;
&lt;li&gt;The full keyfile path, using the USB device's UUID: &lt;code&gt;/dev/disk/by-uuid/81D6-413D:/secure.key&lt;/code&gt; (must be set).&lt;/li&gt;
&lt;li&gt;The necessary options for &lt;code&gt;cryptsetup&lt;/code&gt; to invoke the keyscript (must be set).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tells the system where to find the keyfile and how to decrypt at boot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/crypttab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s very likely that your system has an entry like this by default (note &lt;code&gt;luks&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dm_crypt-0 UUID=XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX none luks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I suggest that you &lt;strong&gt;save it&lt;/strong&gt; somewhere or comment it putting &lt;code&gt;#&lt;/code&gt; at the beginning of the line.&lt;/p&gt;

&lt;p&gt;The first thing that you see in the line is LUKS' device name that we will also need later to add the keyfile inside, so also save it. Common names are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dm_crypt-0 / cryptdrive (Ubuntu)&lt;/li&gt;
&lt;li&gt;cryptdata (Pop!_OS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ &lt;strong&gt;It's crucial&lt;/strong&gt; to preserve the original device name (e.g.: dm_crypt-0), UUID, and exact field spacing to avoid boot issues.&lt;/p&gt;

&lt;p&gt;Replace (&lt;strong&gt;not add, duplicate UUIDs or device names ºare not allowed&lt;/strong&gt;) your entry to match the changes of this entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dm_crypt-0 UUID=XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX /dev/disk/by-uuid/81D6-413D:/secure.key luks,discard,cipher=aes-xts-plain64,size=256,hash=sha1,keyscript=/lib/cryptsetup/scripts/keyscript.sh,tries=4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ The &lt;code&gt;tries=4&lt;/code&gt; option is &lt;strong&gt;mandatory&lt;/strong&gt; — it ensures the system runs the script 3 times and gives it a chance to gracefully fall back on the 4th try. That 4th attempt is where the script prompts the user to manually enter the password, in case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The USB isn't connected or could not be properly mounted.&lt;/li&gt;
&lt;li&gt;The keyfile isn't found.&lt;/li&gt;
&lt;li&gt;The keyfile is invalid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Double-check the USB UUID and keyscript path, especially if you changed the script location.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Get LUKS' device path
&lt;/h2&gt;

&lt;p&gt;With LUKS' device name (e.g.: dm_crypt-0) from the &lt;code&gt;crypttab&lt;/code&gt; entry in previous step, we will get its device path to add the keyfile into it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup status dm_crypt-0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line that we need from the output is: &lt;code&gt;device: /dev/nvme0n1p3&lt;/code&gt; where &lt;code&gt;/dev/nvme0n1p3&lt;/code&gt; is the device path that we need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dev/mapper/dm_crypt-0 is active and is in use.
 type:    LUKS2
 cipher:  aes-xts-plain64
 keysize: 512 bits
 key location: keyring
 device:  /dev/nvme0n1p3
 sector size:  512
 offset:  32768 sectors
 size:    1993975808 sectors
 mode:    read/write
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Add the keyfile to LUKS device
&lt;/h2&gt;

&lt;p&gt;Here we need to set LUKS' device path that we got in the previous step and specify the keyfile that we generated at the very beginning, in step 1.&lt;/p&gt;

&lt;p&gt;This will require an existing passphrase to add the new keyfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup luksAddKey /dev/nvme0n1p3 /media/usb/secure.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If "nothing happens" (it actually returns a shell success code) then it worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Test the keyfile
&lt;/h2&gt;

&lt;p&gt;To test that the previous step was done correctly, we can do the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup luksOpen &lt;span class="nt"&gt;--test-passphrase&lt;/span&gt; &lt;span class="nt"&gt;--key-file&lt;/span&gt; /media/usb/secure.key /dev/nvme0n1p3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it works, it won't show anything (only return success in the terminal). If it fails, it will show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No key available with this passphrase.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Update initramfs
&lt;/h2&gt;

&lt;p&gt;The last step to set everything up is updating initramfs. Initramfs is a temporary file system with a mini-Linux inside used only during boot. After boot everything is deleted.&lt;/p&gt;

&lt;p&gt;Updating initramfs will add to the initramfs image the updated version of &lt;code&gt;/etc/crypttab&lt;/code&gt; together with the unlock script that we've previously downloaded inside &lt;code&gt;/lib/cryptsetup&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: If you see any errors while running the command, it's better to revert to your original &lt;code&gt;/etc/crypttab&lt;/code&gt; entry and post a comment in the comments section below this post.&lt;/p&gt;

&lt;p&gt;This may take around 1 min:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;update-initramfs &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check that our script was added inside initramfs we can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;lsinitramfs /boot/initrd.img-XXXXXXXX-generic | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'keyscript.sh'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lsinitramfs&lt;/code&gt; lists the files of initramfs' compressed image. So all the files of the mini-Linux will be listed there.&lt;/p&gt;

&lt;p&gt;And something like this should be printed in our console (you can Ctrl-C to cancel the grep after finding it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;usr/lib/cryptsetup/scripts/keyscript.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Reboot
&lt;/h2&gt;

&lt;p&gt;This is the last necessary step.&lt;/p&gt;

&lt;p&gt;Simply reboot the USB and your setup should be automatically decrypted! 🎉🥳&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all went well, please comment below with your Ubuntu (or other distro) version and your cryptsetup version.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Logs
&lt;/h2&gt;

&lt;p&gt;Something cool that I figured out is a way to log messages inside initramfs. Usually this is almost impossible because, as I said earlier, initramfs (a temporary filesystem) is cleaned up right after boot, so anything written anywhere will be removed.&lt;/p&gt;

&lt;p&gt;I realized that logs can be sent directly to the kernel using &lt;code&gt;/dev/kmsg&lt;/code&gt;, so that later on, after boot, you can grep logs doing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dmesg | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'keyscript.sh'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is pretty cool because either on success or failure (you had to manually enter the password) you can check the logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logs when using the USB key normally:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[    8.199775] keyscript.sh: Attempt #1
[    8.213481] keyscript.sh: Using keyfile: /mnt/unlock-usb/secure.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Logs when USB device was not found or simply choosing to manually introduce the password
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[    3.258277] keyscript.sh: Attempt #1
[    6.271922] keyscript.sh: Device for decryption not found: /dev/disk/by-uuid/81D6-413D
[    6.941412] keyscript.sh: Proceeding to ask for manually entering the password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Logs when plugging in a USB without the keyfile
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[    3.234567] keyscript.sh: Attempt #1
[    6.274412] keyscript.sh: Keyfile not found at: /mnt/unlock-usb/secure.key
[    6.954763] keyscript.sh: Proceeding to ask for manually entering the password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Logs when the USB could not be mounted on boot
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[    3.112245] keyscript.sh: Attempt #1
[    6.514122] keyscript.sh: Failed to mount device: /dev/disk/by-uuid/81D6-413D at /mnt/unlock-usb
[    6.613212] keyscript.sh: Proceeding to ask for manually entering the password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Logs when the keyfile is located but incorrect
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[    8.082461] keyscript.sh: Attempt #1
[    8.096964] keyscript.sh: Using keyfile: /mnt/unlock-usb/secure.key
[   13.501285] keyscript.sh: Attempt #2
[   13.511812] keyscript.sh: Using keyfile: /mnt/unlock-usb/secure.key
[   18.349567] keyscript.sh: Attempt #3
[   18.361704] keyscript.sh: Using keyfile: /mnt/unlock-usb/secure.key
[   23.205031] keyscript.sh: Max retries (3) reached. Proceeding to ask for manually entering the password.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Share thoughts!
&lt;/h1&gt;

&lt;p&gt;Would it be interesting to consider any USB as valid? Or it's better to restrict it by UUID as we're doing already?&lt;/p&gt;

&lt;p&gt;Special thanks to Guilhem Moulin, from the &lt;a href="https://cryptsetup-team.pages.debian.net/cryptsetup/README.initramfs.html" rel="noopener noreferrer"&gt;official Cryptsetup Team at Debian&lt;/a&gt;, for helping me clear up some doubts about the behavior of cryptsetup.&lt;/p&gt;

&lt;p&gt;Say thanks if you made it, or help me keep this post updated with new versions of Ubuntu or cryptsetup.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>encryption</category>
      <category>usb</category>
      <category>cryptsetup</category>
    </item>
  </channel>
</rss>
