<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Vaultwarden on Code is cheap, let&#39;s talk</title>
    <link>https://blog.ferstar.org/en/tags/vaultwarden/</link>
    <description>Code is cheap, let&#39;s talk</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <copyright>© 2026 ferstar · [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)</copyright>
    <lastBuildDate>Sat, 08 Aug 2026 10:00:00 +0800</lastBuildDate>
    <ttl>60</ttl><atom:link href="https://blog.ferstar.org/en/tags/vaultwarden/index.xml" rel="self" type="application/rss+xml" /><image>
      <url>https://blog.ferstar.org/site-logo.png</url>
      <title>Code is cheap, let&#39;s talk</title>
      <link>https://blog.ferstar.org/</link>
    </image>
    
    <item>
      <title>1Password Raised Its Price Again, So I Moved to Self-Hosted Vaultwarden</title>
      <link>https://blog.ferstar.org/en/posts/1password-to-self-hosted-vaultwarden/</link>
      <pubDate>Sat, 08 Aug 2026 10:00:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/1password-to-self-hosted-vaultwarden/</guid>
      <description>After my 1Password Families annual price rose from US$59.85 to US$71.88, I moved to self-hosted Vaultwarden and completed the migration of regular credentials and passkeys with backups, invitations, and recovery in place.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>The trigger was simple: 1Password raised its price again.</p>
<p>My three-member Families plan used to cost US$59.85 per year. The billing page showed that the next renewal would be US$71.88 per year, roughly a 20% increase. The price is reasonable, yet I no longer wanted to keep following every subscription increase. So I started looking for an alternative and eventually moved my personal vault to Vaultwarden on a VPS I run myself.</p>
<p>The migration itself was not especially difficult. The time went into everything around it: passkeys follow a different path from ordinary login items, a broken server can lock away the recovery credentials along with the vault, and bulk organization can trigger version conflicts.</p>
<p>This post records one real migration and keeps only the deployment details that matter to the move. The real hostname is replaced with <code>vault.example.com</code> here.</p>

<h2 class="relative group">The short version
    <div id="the-short-version" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-short-version" aria-label="Anchor">#</a>
    </span>
    
</h2>
<ul>
<li>For personal or family use, Vaultwarden is lightweight enough, and the official Bitwarden browser, desktop, and mobile clients can still be used.</li>
<li>Regular data such as passwords, usernames, OTPs, and notes migrated successfully; the in-app Android import moved everything, including passkeys, in this real-world test.</li>
<li>I did not use <code>.1pux</code> or any export file in this migration. Everything was imported directly from another app on Android, including regular data and passkeys.</li>
<li>1Password should not be deleted immediately after the first successful import. The real completion criteria are a working new client, readable backups, and a recovery path that does not depend on the vault itself.</li>
</ul>
<pre class="not-prose mermaid">
flowchart LR
    A["1Password"] --> B["Export / CXP"]
    B --> C["Vaultwarden"]
    C --> D["Client verification"]
    C --> E["Backups"]
    D --> F["Keep 1Password until verified"]
</pre>


<h2 class="relative group">Why Vaultwarden
    <div id="why-vaultwarden" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#why-vaultwarden" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>I compared three routes at the time:</p>
<ul>
<li><strong>Vaultwarden</strong>: lightweight, with a complete Bitwarden client ecosystem, and a good fit for personal or family use. Its server is an independent implementation, so compatibility needs to be checked against the actual versions in use.</li>
<li><strong>Passbolt</strong>: stronger team permissions, auditing, and sharing, but with a heavier deployment and more supporting services.</li>
<li><strong>KeePassXC</strong>: the smallest server trust surface, but essentially a synchronized database file, which is less natural for real-time multi-user sharing.</li>
</ul>
<p>I already had a VPS and 1Panel, so I chose Vaultwarden: keep the data on a dedicated data disk, run it with Docker, and reuse 1Panel/OpenResty for HTTPS and reverse proxying.</p>

<h2 class="relative group">The work that is easy to underestimate
    <div id="the-work-that-is-easy-to-underestimate" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-work-that-is-easy-to-underestimate" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">1. Decide on the data disk and directory first
    <div id="1-decide-on-the-data-disk-and-directory-first" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-decide-on-the-data-disk-and-directory-first" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>I added a 150 GiB disk to the VPS, mounted it at <code>/data</code>, and fixed the Vaultwarden data directory as:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/data/services/vaultwarden</span></span></code></pre></div></div>
<p>I ended up choosing ext4. I did compare XFS in practice: it provided somewhat more free space on an empty disk, but it cannot be shrunk. Since I might move to a smaller disk later, I prioritized room for recovery and migration over saving a little space.</p>
<p>There is no universal answer here. For a password manager, keeping future migration options open matters more than theoretical performance.</p>

<h3 class="relative group">2. DNS, certificates, and the proxy are easier to break than the container
    <div id="2-dns-certificates-and-the-proxy-are-easier-to-break-than-the-container" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-dns-certificates-and-the-proxy-are-easier-to-break-than-the-container" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Before exposing the service, verify that DNS points to the right VPS. I hit an old IPv6 record during the process, so requests were not reaching the new machine even though the container itself was fine.</p>
<p>In the final setup, 1Panel manages the certificate for <code>vault.example.com</code>, using the existing Cloudflare DNS account for issuance and renewal. OpenResty reverse-proxies the hostname to Vaultwarden. This leaves one certificate manager instead of having standalone certbot and the panel maintain the same certificate at the same time.</p>
<p>During acceptance, do not check only whether the container is <code>healthy</code>. Check HTTPS from the public internet, redirects, the Vaultwarden health endpoint, and a real login. One later access failure turned out to be a transient DNS problem on my Mac; the remote service had stayed healthy and returned 200 all along.</p>

<h3 class="relative group">3. Separate public sign-up from invitations
    <div id="3-separate-public-sign-up-from-invitations" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-separate-public-sign-up-from-invitations" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>At first I disabled both public sign-up and invitations. When I wanted to share with a friend, I kept public registration closed and enabled invitations only:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-env" data-lang="env"><span class="line"><span class="cl"><span class="nv">SIGNUPS_ALLOWED</span><span class="o">=</span><span class="nb">false</span>
</span></span><span class="line"><span class="cl"><span class="nv">INVITATIONS_ALLOWED</span><span class="o">=</span>true</span></span></code></pre></div></div>
<p>The friend uses their own email address and master password, then joins an Organization and Collection. Personal passwords stay in the personal vault; only items that need to be shared go into the shared collection.</p>
<p>Invitation email also requires SMTP. I used Mailgun SMTP and ran two separate checks: first a STARTTLS login handshake, then a real invitation email. A successful handshake only confirms SMTP authentication; delivery still needs its own test, so record the two results separately.</p>

<h2 class="relative group">The migration route that worked
    <div id="the-migration-route-that-worked" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-migration-route-that-worked" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">Step 1: Keep 1Password around
    <div id="step-1-keep-1password-around" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#step-1-keep-1password-around" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Before migrating, make sure the 1Password account still unlocks normally and prepare an encrypted backup. A successful login to the new service alone does not confirm that the old data is safely migrated.</p>

<h3 class="relative group">Step 2: Import directly inside the Android app
    <div id="step-2-import-directly-inside-the-android-app" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#step-2-import-directly-inside-the-android-app" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>I did not use <code>.1pux</code> or create an export file first. The entire migration happened inside the Bitwarden Android app: configure the self-hosted server, then open:</p>
<p><code>Settings → Vault → Import items → Import from another app → 1Password</code></p>
<p>This path imported both regular data and passkeys completely. The migration relied on the app’s direct app-to-app import and used no exported file.</p>
<p>After importing, check at least the following:</p>
<ul>
<li>whether usernames and passwords are complete;</li>
<li>whether OTPs generate the right codes;</li>
<li>whether notes, custom fields, and attachments are still there;</li>
<li>whether folders need to be redesigned;</li>
<li>whether a second import accidentally created duplicates.</li>
</ul>
<p>For a first run, it is still sensible to sample a small number of important items, verify login, autofill, and passkeys on the new device, and keep the old 1Password vault available until that check is done.</p>
<p>Useful references:</p>
<ul>
<li><a href="https://bitwarden.com/help/import-from-1password/"  target="_blank" rel="noreferrer">Bitwarden: Import from 1Password</a></li>
<li><a href="https://github.com/dani-garcia/vaultwarden"  target="_blank" rel="noreferrer">Vaultwarden project</a></li>
</ul>

<h3 class="relative group">Step 3: Accept each client separately
    <div id="step-3-accept-each-client-separately" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#step-3-accept-each-client-separately" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The browser extension, desktop app, and mobile app all need the self-hosted server selected before login. The address looks like this:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">https://vault.example.com</span></span></code></pre></div></div>
<p>I also tested the Bitwarden CLI. The minimal self-hosted configuration path is:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">bw config server https://vault.example.com
</span></span><span class="line"><span class="cl">bw config server
</span></span><span class="line"><span class="cl">bw login your-email@example.com
</span></span><span class="line"><span class="cl">bw status</span></span></code></pre></div></div>
<p>There was a client-side trap unrelated to the server: at the time, the CLI installed globally with pnpm failed under Node 26.3.0 with <code>Cannot find module 'buffer/'</code>. The actual problem was the combination of the global dependency links and the runtime. Installing the same CLI version with npm and running it with Node 22 fixed it.</p>
<p>When the CLI fails, check how <code>bw</code> was installed, which Node version it is using, and which server it is targeting before restarting Vaultwarden.</p>

<h2 class="relative group">Vault organization taught me a practical lesson
    <div id="vault-organization-taught-me-a-practical-lesson" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#vault-organization-taught-me-a-practical-lesson" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>After the migration, I wanted to reorganize 261 records that had all been sitting in <code>No Folder</code>. Classification was easy; bulk updates were not.</p>
<p>I made an encrypted backup first, then changed only folder ownership. I did not edit passwords, notes, or OTPs, and I did not delete duplicates. During concurrent updates, a version race in Vaultwarden temporarily moved six same-name Mattermost items to the trash. After resyncing and restoring them one by one, the item IDs matched the backup again.</p>
<p>That left me with a few rules:</p>
<ol>
<li>Make a recoverable backup before bulk writes.</li>
<li>Treat a one-field update as a real data change.</li>
<li>Concurrent requests are not always faster; stale client copies can create races.</li>
<li>Final verification should compare totals, item IDs, and the trash, not just HTTP 200.</li>
</ol>
<p>The vault is now organized into Work and Internal, Development and Cloud Services, Personal Accounts, Communication and Social, Finance and Identity, Home and Devices, and Security and Keys. All 261 items left <code>No Folder</code>. I did not delete duplicates without review.</p>

<h2 class="relative group">A backup is not “export one JSON file”
    <div id="a-backup-is-not-export-one-json-file" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#a-backup-is-not-export-one-json-file" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Self-hosting changes the psychology of the problem: you own the recovery responsibility.</p>
<p>This is how I closed the loop:</p>
<ul>
<li>the database, attachments, and configuration live under <code>/data</code>;</li>
<li>the VPS creates backups on a schedule;</li>
<li>only after the local backup succeeds, it syncs over SSH to another host, <code>DO_NEW</code>;</li>
<li>backup files use an encrypted format and produce verifiable results;</li>
<li>recovery credentials for the cloud account, DNS, email, and VPS are not all stored in this vault; at least one independent break-glass path remains.</li>
</ul>
<p>A copy stored only on the same VPS provides no off-site protection. A backup also needs a restore test before it can be trusted. Perform at least one temporary restore and verify the database, attachments, configuration, and client login.</p>

<h2 class="relative group">Final judgment
    <div id="final-judgment" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#final-judgment" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>This migration convinced me that Vaultwarden can replace 1Password for personal or family use. Starting the container is only the first step.</p>
<p>You give up part of the managed service and take on DNS, TLS, SMTP, upgrades, logs, backups, and recovery. Ordinary password data moves easily. Passkeys, sharing permissions, and disaster recovery are where the careful work belongs.</p>
<p>If the only goal is to save the subscription fee and you do not want to maintain backups and recovery, the trade-off may not be worthwhile. If you care more about data control and client freedom, and are willing to operate it like a small production service, Vaultwarden is a practical option.</p>
<p>For me, migration was complete only when all four answers were yes:</p>
<ul>
<li>a new device can log in and autofill;</li>
<li>OTPs and important attachments have been sampled;</li>
<li>passkeys have been verified in the target client, or have an explicit site-by-site recreation plan;</li>
<li>if the VPS disappears, there is still an independent recovery route and a verifiable backup.</li>
</ul>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
