<?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: Alphasec</title>
    <description>The latest articles on DEV Community by Alphasec (@alphasec).</description>
    <link>https://dev.to/alphasec</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3890949%2F3af0bd1c-221d-4e11-b1a4-afb2d22e198e.png</url>
      <title>DEV Community: Alphasec</title>
      <link>https://dev.to/alphasec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alphasec"/>
    <language>en</language>
    <item>
      <title>Turn Any Website into LLM-Ready Markdown with Firecrawl and Python</title>
      <dc:creator>Alphasec</dc:creator>
      <pubDate>Tue, 10 Nov 2026 08:20:07 +0000</pubDate>
      <link>https://dev.to/alphasec/turn-any-website-into-llm-ready-markdown-with-firecrawl-and-python-4i9j</link>
      <guid>https://dev.to/alphasec/turn-any-website-into-llm-ready-markdown-with-firecrawl-and-python-4i9j</guid>
      <description>&lt;p&gt;If you're feeding web content to an LLM, raw HTML wastes most of your context window on navigation, scripts and markup. What you want is the article text as markdown, with headings and code blocks intact. &lt;a href="https://www.firecrawl.dev/referral?rid=XNXRER8X" rel="noopener noreferrer"&gt;Firecrawl&lt;/a&gt; does that conversion as an API, including JavaScript-rendered pages, so you don't maintain a scraper per site.&lt;/p&gt;

&lt;p&gt;This post walks through three jobs: converting a single page, saving a list of URLs as markdown files, and crawling a whole docs site into a folder you can index for RAG.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Install the SDK. I pin the version so the examples keep working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip install &lt;span class="nv"&gt;firecrawl&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;4.55.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can try &lt;code&gt;scrape&lt;/code&gt; without an API key, at a low per-IP rate limit. For anything beyond a quick test, create a free account (1,000 credits a month, one credit per page) and export the key:&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="kd"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;FIRECRAWL_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"fc-YOUR_API_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Convert a Single Page to Markdown
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;firecrawl&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Firecrawl&lt;/span&gt;

&lt;span class="n"&gt;firecrawl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Firecrawl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# reads FIRECRAWL_API_KEY&lt;/span&gt;

&lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firecrawl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://docs.firecrawl.dev/introduction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formats&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"markdown"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;only_main_content&lt;/span&gt;&lt;span class="o"&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;only_main_content=True&lt;/code&gt; is the setting that matters most for LLM use. It strips headers, footers, cookie banners and sidebars, and on a typical docs page that's a large share of the text. It's on by default, but I set it explicitly so nobody reading the code has to guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save a List of URLs as Markdown Files
&lt;/h2&gt;

&lt;p&gt;For a handful of known pages, &lt;code&gt;batch_scrape&lt;/code&gt; sends them in one job instead of a loop of single calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;

&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;firecrawl&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Firecrawl&lt;/span&gt;

&lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"https://docs.firecrawl.dev/features/scrape"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"https://docs.firecrawl.dev/features/crawl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"https://docs.firecrawl.dev/features/map"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;firecrawl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Firecrawl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firecrawl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch_scrape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;formats&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"markdown"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poll_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pages"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"_"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;"index"&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{slug}.md"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;markdown&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"saved {slug}.md"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Naming files after the URL path keeps the mapping obvious when you index them later, and makes it easy to cite the source page in an answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crawl an Entire Docs Site into a Folder
&lt;/h2&gt;

&lt;p&gt;Before crawling, map the site. &lt;code&gt;map&lt;/code&gt; returns the URLs it can find, fast and cheaply, so you know what the crawl will cost before you spend the credits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;firecrawl&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Firecrawl&lt;/span&gt;

&lt;span class="n"&gt;firecrawl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Firecrawl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firecrawl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://docs.firecrawl.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the count looks right, crawl with a &lt;code&gt;limit&lt;/code&gt; and exclude the sections you don't need. Changelogs and blog archives are the usual ones to skip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;

&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;firecrawl&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Firecrawl&lt;/span&gt;

&lt;span class="n"&gt;firecrawl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Firecrawl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firecrawl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crawl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://docs.firecrawl.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;exclude_paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"blog/.+"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"changelog.*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scrape_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"formats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"markdown"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"only_main_content"&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="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"docs"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"_"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;"index"&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{slug}.md"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;markdown&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{job.status}: {len(job.data)} pages"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;crawl&lt;/code&gt; blocks until the job finishes. For big sites, use &lt;code&gt;start_crawl&lt;/code&gt; to get a job ID back immediately and check it with &lt;code&gt;get_crawl_status&lt;/code&gt;, so a long crawl doesn't tie up your script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips Before You Index Anything
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;limit&lt;/code&gt; on every crawl. Without it, the crawl follows every link it finds, and each page costs a credit. Excluding paths up front matters for the same reason: pages you filter out after the crawl have already been paid for.&lt;/p&gt;

&lt;p&gt;Check for near-empty pages. Login walls and bot challenges can come back as a few lines of text, and indexing them adds noise to every search. Skipping files under a few hundred characters catches most of them.&lt;/p&gt;

&lt;p&gt;When you chunk the files for embeddings, split on headings rather than a fixed character count. Firecrawl keeps the page's heading structure, so splitting on &lt;code&gt;##&lt;/code&gt; gives you chunks that each cover one topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Go Next
&lt;/h2&gt;

&lt;p&gt;Once the markdown is on disk, any embedding pipeline can index it. If you want structured fields instead of text, Firecrawl's &lt;code&gt;scrape&lt;/code&gt; also takes a JSON schema. I cover that and the crawl, map and agent endpoints in more detail in &lt;a href="https://alphasec.io/supercharge-web-scraping-and-extraction-with-firecrawl/" rel="noopener noreferrer"&gt;this Firecrawl guide on alphasec.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post includes a referral link: signing up through it gives us both extra credits.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Stop Hiring, Start Orchestrating: Running an AI Agent Company with Paperclip on Railway</title>
      <dc:creator>Alphasec</dc:creator>
      <pubDate>Sat, 16 May 2026 09:02:51 +0000</pubDate>
      <link>https://dev.to/alphasec/stop-hiring-start-orchestrating-running-an-ai-agent-company-with-paperclip-on-railway-41bn</link>
      <guid>https://dev.to/alphasec/stop-hiring-start-orchestrating-running-an-ai-agent-company-with-paperclip-on-railway-41bn</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a cross-post from alphasec.io. Original post &lt;a href="https://alphasec.io/stop-hiring-start-orchestrating-running-an-ai-agent-company-with-paperclip-on-railway" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The idea of a one-person company isn't new. What's new though is the idea of a one-person company where the person isn't doing most of the actual work. AI agents are. You set the direction, define the roles and budgets, and the agents handle the execution — writing, researching, analysing, reporting — on a schedule, without being asked twice. And tools like Paperclip are making it real enough to run today.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Paperclip?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://paperclip.ing/?ref=alphasec.io" rel="noopener noreferrer"&gt;Paperclip&lt;/a&gt; describes itself as "the human control plane for AI labor." You define a company — give it a mission, a budget, a set of agent personas — and Paperclip runs it. Agents get heartbeats on a schedule, pick up open tasks, call their assigned large language model (LLM), and post results back as issue comments. It's effectively a project management tool where every team member is an AI agent.&lt;/p&gt;

&lt;p&gt;Paperclip started as an experiment in running fully autonomous agentic operations at scale — "&lt;a href="https://github.com/paperclipai/paperclip?ref=alphasec.io" rel="noopener noreferrer"&gt;open-source&lt;/a&gt; orchestration for zero-human companies." It's evolving into something broader: a full agent orchestration platform for building companies that run on autonomous agents rather than headcount. The pivot is still in motion, but the core idea is already compelling enough to deploy and explore.&lt;/p&gt;

&lt;p&gt;The unit of organisation is a &lt;strong&gt;company&lt;/strong&gt;. Inside a company you have &lt;strong&gt;agents&lt;/strong&gt; (personas with roles, adapters, and heartbeat schedules), &lt;strong&gt;projects&lt;/strong&gt;, and &lt;strong&gt;issues&lt;/strong&gt; (tasks). An agent wakes up when its heartbeat fires, looks at what's assigned to it, does the work, and goes back to sleep. The idea of a pre-built company is quite powerful, and the ecosystem is slowly growing around &lt;a href="https://companies.sh/?ref=alphasec.io" rel="noopener noreferrer"&gt;companies.sh&lt;/a&gt; — pre-built company definitions you can import into Paperclip with a single command. Imagine an external penetration testing team, a content agency, or a software development shop, fully staffed with AI agents, importable in seconds.&lt;/p&gt;

&lt;p&gt;To make it concrete, here's what a lean &lt;strong&gt;content agency&lt;/strong&gt; could look like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Heartbeat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Editor in Chief&lt;/td&gt;
&lt;td&gt;Reviews briefs, assigns stories, sets editorial direction&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Research Analyst&lt;/td&gt;
&lt;td&gt;Monitors sources, surfaces stories, drafts research notes&lt;/td&gt;
&lt;td&gt;Every 6h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writer&lt;/td&gt;
&lt;td&gt;Drafts posts from research notes and briefs&lt;/td&gt;
&lt;td&gt;On demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO Analyst&lt;/td&gt;
&lt;td&gt;Reviews drafts for keyword coverage and structure&lt;/td&gt;
&lt;td&gt;On demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publisher&lt;/td&gt;
&lt;td&gt;Formats and schedules final output&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every agent is backed by an LLM of your choice. The Editor in Chief might run on Claude for judgment-heavy decisions; the Research Analyst on Gemini Flash for speed and cost. Each agent has its own adapter, its own system prompt, its own task queue. Paperclip keeps them coordinated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying Paperclip on Railway
&lt;/h3&gt;

&lt;p&gt;This section covers the deployment of Paperclip on &lt;a href="https://railway.com/?referralCode=alphasec" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; — a managed app hosting platform that handles infrastructure, TLS, and deployments without requiring server management. If you want a VPS approach with a fixed monthly cost, the &lt;a href="https://alphasec.io/stop-hiring-start-orchestrating-running-an-ai-agent-company-with-paperclip-on-digitalocean/" rel="noopener noreferrer"&gt;DigitalOcean guide&lt;/a&gt; covers that instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; A Railway account, and a valid API key from at least one LLM provider (Anthropic, OpenAI, or Gemini).&lt;/p&gt;

&lt;p&gt;Paperclip runs from a custom &lt;code&gt;Dockerfile&lt;/code&gt; that wraps the official Paperclip image without replacing its startup chain, so &lt;code&gt;tini&lt;/code&gt;, the upstream entrypoint, and the production server all run exactly as upstream ships them. A small &lt;code&gt;entrypoint.sh&lt;/code&gt; handles first-boot configuration, writing a minimal &lt;code&gt;config.json&lt;/code&gt;, printing the admin invite to the logs, and closing sign-up once the admin account exists. An earlier version ran Paperclip's development server, which idled at about 3GB and returned 403 to Railway's health check; the production server idles at about 1GB and answers &lt;code&gt;/api/health&lt;/code&gt;, which the template now uses as its health check, with the restart policy set to Always.&lt;/p&gt;

&lt;p&gt;The template provisions Paperclip with a managed Railway Postgres database already wired to &lt;code&gt;DATABASE_URL&lt;/code&gt;, a persistent volume at &lt;code&gt;/paperclip&lt;/code&gt;, and a public domain. Expect about $10-15/month at idle, more while agents are working.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://railway.com/deploy/paperclip-ai-agent-company?referralCode=alphasec" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdza8fa0foe3c7h7osnqq.png" alt="Deploy on Railway" width="366" height="80"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Post-Deployment Setup and Connection
&lt;/h3&gt;

&lt;p&gt;Railway routes traffic through its edge to port 3100 internally — no port configuration needed in the public URL. Once the service is running, the admin invite URL appears in the service logs. In the Railway dashboard, go to your deployed service → View logs → Deploy logs and search for invite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;◆  Created bootstrap CEO invite.
│  Invite URL: https://paperclip-xxxx.up.railway.app/invite/pcp_bootstrap_...
│  Expires: &amp;lt;72 hours after first boot&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the URL to create your admin account through Paperclip's native setup screen. Sign-up closes automatically once your admin account exists: the service restarts once, and the logs show &lt;code&gt;entrypoint: admin claimed, restarting to disable sign-up&lt;/code&gt;. To keep sign-up open for teammates, set &lt;code&gt;PAPERCLIP_AUTH_DISABLE_SIGN_UP=false&lt;/code&gt; in Railway Variables.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7g7nzruk262zicg19ynb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7g7nzruk262zicg19ynb.png" alt="Paperclip dashboard" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A couple of adapters work out of the box with environment variables — no interactive login required:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Adapter in Paperclip&lt;/th&gt;
&lt;th&gt;Environment Variable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude_local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opencode_local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OPENAI_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opencode_local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GEMINI_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For Gemini, select &lt;code&gt;opencode_local&lt;/code&gt; as the adapter and choose your model (e.g. &lt;code&gt;gemini-3.8-flash&lt;/code&gt;) in the agent settings. The &lt;code&gt;gemini_local&lt;/code&gt; adapter requires interactive OAuth and won't work in a Railway container. Similarly, the &lt;code&gt;codex_local&lt;/code&gt; adapter needs WebSocket-based authentication that can't be completed headlessly — stick with &lt;code&gt;opencode_local&lt;/code&gt; for OpenAI models.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Next
&lt;/h3&gt;

&lt;p&gt;The deployment covered here is just the start — Paperclip running cleanly, agents wired up, ready to receive tasks. The real interesting (and fun) part lies in what you build on top of it. The &lt;a href="https://github.com/alphasecio/paperclip" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; also includes a &lt;code&gt;digitalocean/&lt;/code&gt; folder with a single-script VPS deployment if you prefer a fixed-cost server. I'm fascinated by the companies.sh concept, and I'm exploring a purpose-built agent company - a threat intelligence team that monitors security advisories, extracts IOCs and TTPs, and generates detection rules - as a follow up. If that sounds exciting, it'll be next.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>selfhosted</category>
      <category>docker</category>
      <category>ai</category>
    </item>
    <item>
      <title>A Developer Built an Open-Source Dropbox on a Dare. Here's How to Self-Host It.</title>
      <dc:creator>Alphasec</dc:creator>
      <pubDate>Tue, 21 Apr 2026 16:23:55 +0000</pubDate>
      <link>https://dev.to/alphasec/a-developer-built-an-open-source-dropbox-on-a-dare-heres-how-to-self-host-it-39mi</link>
      <guid>https://dev.to/alphasec/a-developer-built-an-open-source-dropbox-on-a-dare-heres-how-to-self-host-it-39mi</guid>
      <description>&lt;p&gt;A few weeks ago, &lt;a href="https://x.com/swyx" rel="noopener noreferrer"&gt;@swyx&lt;/a&gt; nerd-sniped &lt;a href="https://x.com/zachmeyer" rel="noopener noreferrer"&gt;@zachmeyer&lt;/a&gt; into building an open-source Dropbox. Zach took it seriously, and the result is &lt;a href="https://locker.dev/" rel="noopener noreferrer"&gt;Locker&lt;/a&gt;: a self-hostable file storage platform that covers most of what you'd actually use Dropbox or Google Drive for, without the subscription or lock-in.&lt;/p&gt;

&lt;p&gt;I came across the thread on X, spent some time getting Locker running on &lt;a href="https://railway.com/?referralCode=alphasec" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;, and figured the deployment notes were worth writing up — especially since the setup has a few non-obvious pieces that trip you up if you're looking to self-host.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Locker Actually Is
&lt;/h2&gt;

&lt;p&gt;Locker is a Dockerized Next.js application backed by PostgreSQL. The &lt;a href="https://github.com/zmeyer44/Locker" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; is worth a look — the tech stack is modern and clean: Next.js 16 App Router, tRPC for end-to-end type safety, Drizzle ORM, BetterAuth, and Tailwind CSS, organized as a Turborepo monorepo with pnpm workspaces.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyhn9zxtntrkhqp79fxfe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyhn9zxtntrkhqp79fxfe.png" alt="Locker" width="799" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feature-wise it covers the things you'd actually miss from the commercial alternatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File and folder management&lt;/strong&gt; — upload, rename, move, delete, with a familiar explorer UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share links&lt;/strong&gt; — password protection, expiry dates, and download limits per link&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload links&lt;/strong&gt; — let anyone send you files without an account, useful for collecting documents from clients or collaborators&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage provider agnostic&lt;/strong&gt; — switch between local disk, AWS S3, Cloudflare R2, or Vercel Blob via a single environment variable, no code changes required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locker as an S3 bucket&lt;/strong&gt; — generate Locker API keys and use them in other applications to write data directly to your Locker instance; it speaks S3-compatible protocol&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-user storage quotas&lt;/strong&gt; — set limits per user with usage tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workspace teams&lt;/strong&gt; — invite team members with role-based access and organise files across workspaces&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QMD semantic search&lt;/strong&gt; (optional plugin) — search inside the content of your files, not just filenames&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FTS full-text search&lt;/strong&gt; (optional plugin) — full-text search across your stored documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email/password and Google OAuth&lt;/strong&gt; — authentication handled by BetterAuth, sessions managed server-side&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API keys&lt;/strong&gt; — programmatic access for building integrations and automating workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual bash shell&lt;/strong&gt; — navigate your file tree with &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cd&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, and &lt;code&gt;grep&lt;/code&gt; via a terminal panel; reads your actual stored files lazily from the configured storage provider&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last one is either delightful or unnecessary depending on your personality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Storage Provider Thing Is the Key Insight
&lt;/h2&gt;

&lt;p&gt;Most self-hosted file storage tools tie you to a specific backend. Locker doesn't. You set &lt;code&gt;BLOB_STORAGE_PROVIDER&lt;/code&gt; in your environment and point it at wherever you want files to live:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;What you need&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local disk&lt;/td&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Just a directory path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS S3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Access key, secret, region, bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare R2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Account ID, keys, bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel Blob&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vercel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A read/write token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you already have an S3 bucket, you can point Locker at it and immediately have a UI over your existing data. If you're starting fresh, local disk works out of the box. Switching later is one variable change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying Locker
&lt;/h2&gt;

&lt;p&gt;Locker is designed to run with Docker Compose — a &lt;code&gt;migrate&lt;/code&gt; container runs the database migrations first, then the &lt;code&gt;web&lt;/code&gt; container starts once migrations complete. That's the intended flow.&lt;/p&gt;

&lt;p&gt;Deploying to &lt;a href="https://railway.com/?referralCode=alphasec" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; takes a bit more work because Railway runs a single container rather than orchestrating multiple services. I spent some time getting this right: the key issue is that migrations need to run before the app starts, and the Dockerfile in the repo builds a Next.js standalone output that doesn't include the migration tooling in the final image by default.&lt;/p&gt;

&lt;p&gt;The solution was a custom &lt;code&gt;Dockerfile.railway&lt;/code&gt; that copies the migration dependencies into the runner stage and runs them as part of the startup command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;CMD&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sh"&lt;/span&gt;, &lt;span class="s2"&gt;"-c"&lt;/span&gt;, &lt;span class="s2"&gt;"cd /app/packages/database &amp;amp;&amp;amp; pnpm drizzle-kit migrate &amp;amp;&amp;amp; cd /app &amp;amp;&amp;amp; node apps/web/server.js"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drizzle tracks which migrations have already been applied, so this is idempotent — subsequent deploys only apply new migrations and skip the rest.&lt;/p&gt;

&lt;p&gt;To deploy it yourself, use the Dockerfile change above with Railway’s GitHub deploy, or the project’s Docker Compose setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;After deployment you get a clean file management interface, workspace support for teams, and the ability to generate share links for any file or folder. The virtual bash shell is accessible via a terminal panel and lets you navigate your file tree with standard Unix commands — which turns out to be genuinely useful when you want to script something against your stored files.&lt;/p&gt;

&lt;p&gt;Authentication supports email/password out of the box, and you can add Google OAuth by dropping in &lt;code&gt;GOOGLE_CLIENT_ID&lt;/code&gt; and &lt;code&gt;GOOGLE_CLIENT_SECRET&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is It Ready for Production?
&lt;/h2&gt;

&lt;p&gt;For personal use and small teams, maybe yes. For a large organisation relying on it as primary infrastructure, I'd want to see more production mileage first — the project is still relatively young. That said, the tech choices are solid, the codebase is readable, and the maintainer is active.&lt;/p&gt;

&lt;p&gt;The hosted version at &lt;a href="https://locker.dev/" rel="noopener noreferrer"&gt;locker.dev&lt;/a&gt; is available if you want to try it before committing to self-hosting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;I write about cloud, security, privacy, and self-hosted infrastructure at &lt;a href="https://alphasec.io/" rel="noopener noreferrer"&gt;alphasec.io&lt;/a&gt;. If &lt;a href="https://railway.com/?referralCode=alphasec" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; templates are your thing, I maintain a collection covering everything from starter kits to AI apps and security tools.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
